The Subscriber program manages subscription tiers for Switchboard oracle data access. Users burn payment tokens (SWTCH or other configured tokens) to subscribe to different service levels that control data delivery latency, rate limits, and streaming capacity. Each accepted payment token has a configurable multiplier that affects subscription duration.
// TBD - Will be generated during deployment
declare_id!("TBD");Seeds: ["SUBSCRIBER_STATE"]
Purpose: Global program configuration and admin management
Fields:
#[account(zero_copy)]
pub struct SubscriberState {
pub bump: u8,
padding1: [u8; 7],
// Admin management
pub authority: Pubkey, // Primary admin authority
pub admin_list: [Pubkey; 8], // Additional admins who can modify configs
pub admin_count: u8, // Number of active admins
padding2: [u8; 7],
// Override accounts (permanent free access)
pub override_list: [Pubkey; 32], // Whitelisted accounts with permanent access
pub override_count: u8, // Number of active overrides
padding3: [u8; 7],
// Pricing curve parameters - these control the burn rate
pub base_price_per_epoch: u64, // Base price in lamports per epoch (normalized to SWTCH)
// Price multipliers (in basis points, 10000 = 1x)
// Delay pricing uses a linear curve from max to min based on delay_ms:
// multiplier = delay_max_multiplier - (delay_ms * delay_multiplier_slope)
// Clamped to [delay_min_multiplier, delay_max_multiplier]
pub delay_max_multiplier: u16, // Multiplier at 0ms delay (no delay, most expensive, e.g., 100x = 1000000)
pub delay_min_multiplier: u16, // Minimum multiplier at max delay (cheapest, e.g., 1x = 10000)
pub delay_multiplier_slope: u16, // Multiplier decrease per ms of delay (basis points, e.g., 10 = 0.001x per ms)
// Request rate multipliers - cost per request/minute
pub oracle_request_multiplier_per_req: u16, // Additional cost per oracle request/min (basis points)
pub crossbar_request_multiplier_per_req: u16, // Additional cost per crossbar request/min (basis points)
pub feed_limit_multiplier_per_feed: u16, // Additional cost per unique feed
pub asset_stream_multiplier_per_asset: u16, // Additional cost per streamed asset
padding4: [u8; 4],
// Payment token configuration
pub accepted_tokens: [AcceptedToken; 16], // List of accepted payment tokens
pub accepted_token_count: u8, // Number of configured tokens
padding5: [u8; 7],
// Epoch configuration (Solana epochs or custom epochs)
pub use_solana_epochs: u8, // 1 = use Solana epochs, 0 = use custom epochs
pub custom_epoch_length_seconds: u64, // If custom epochs, length in seconds
padding6: [u8; 7],
// Reserved for future use
_reserved: [u8; 256],
}
// Configuration for each accepted payment token
#[derive(Copy, Clone, AnchorSerialize, AnchorDeserialize)]
pub struct AcceptedToken {
pub mint: Pubkey, // Token mint address
pub burn_vault: Pubkey, // Vault where tokens are sent to be burned
pub time_multiplier: u16, // Multiplier for subscription time (basis points, 10000 = 1x)
pub enabled: u8, // 1 = enabled, 0 = disabled
padding: [u8; 5],
}Seeds: ["SUBSCRIPTION", user_wallet_pubkey]
Purpose: Tracks individual user subscription details
Fields:
#[account(zero_copy)]
pub struct Subscription {
pub bump: u8,
padding1: [u8; 7],
// User identity
pub owner: Pubkey, // Wallet that owns this subscription (primary admin)
// Multi-user access
pub authorized_users: [Pubkey; 16], // Additional users who can use this subscription
pub authorized_user_count: u8, // Number of active authorized users
padding2: [u8; 7],
// Subscription tier settings
pub delay_ms: u16, // Data delay in milliseconds (0 = no delay, 1000 = 1s, 5000 = 5s)
// Request rate limits
pub oracle_requests_per_minute: u16, // Max oracle (on-chain) requests per minute
pub crossbar_requests_per_minute: u16, // Max crossbar (off-chain) requests per minute
pub unique_feed_limit: u16, // Number of unique feeds allowed
pub asset_stream_limit: u16, // Number of assets that can be streamed
padding3: [u8; 2],
// Subscription period tracking
pub subscription_start_epoch: u64, // Epoch when subscription started
pub subscription_end_epoch: u64, // Epoch when subscription expires (rounded down)
pub last_extended_slot: u64, // Last slot when subscription was extended
// Payment tracking
pub total_tokens_burnt: u64, // Total tokens burnt across all payments (lamports)
pub last_burn_amount: u64, // Amount burnt in last transaction
pub last_payment_token: Pubkey, // Mint of token used in last payment
// Reserved for future use
_reserved: [u8; 192],
}Instruction: subscriber_state_init
Purpose: Initialize the global state account
Accounts:
state(init, mut) - SubscriberState PDAauthority(signer) - Initial admin authoritypayer(signer, mut) - Pays for account creationsystem_program
Parameters:
pub struct SubscriberStateInitParams {
pub base_price_per_epoch: u64,
pub delay_max_multiplier: u16,
pub delay_min_multiplier: u16,
pub delay_multiplier_slope: u16,
pub oracle_request_multiplier_per_req: u16,
pub crossbar_request_multiplier_per_req: u16,
pub feed_limit_multiplier_per_feed: u16,
pub asset_stream_multiplier_per_asset: u16,
pub use_solana_epochs: bool,
pub custom_epoch_length_seconds: Option<u64>,
pub initial_tokens: Vec<AcceptedTokenConfig>, // Initial payment tokens to configure
}
pub struct AcceptedTokenConfig {
pub mint: Pubkey,
pub burn_vault: Pubkey,
pub time_multiplier: u16, // Basis points, 10000 = 1x
}Validation:
- Authority must sign
- State must not already exist
- Each token's burn vault must be valid token account for its mint
- time_multiplier must be reasonable (> 0, < 1000000)
- Cannot exceed max token count (16)
- Delay multipliers must satisfy:
delay_min_multiplier < delay_max_multiplier - delay_multiplier_slope should be configured such that reasonable delay values map to the desired range
- Example: To go from 100000 to 10000 over 5000ms: slope = (100000-10000)/5000 = 18
Instruction: subscriber_state_set_config
Purpose: Update pricing curves and configuration
Accounts:
state(mut) - SubscriberState PDAauthority(signer) - Must be authority or in admin_list
Parameters:
pub struct SubscriberStateSetConfigParams {
pub base_price_per_epoch: Option<u64>,
pub delay_max_multiplier: Option<u16>,
pub delay_min_multiplier: Option<u16>,
pub delay_multiplier_slope: Option<u16>,
pub oracle_request_multiplier_per_req: Option<u16>,
pub crossbar_request_multiplier_per_req: Option<u16>,
pub feed_limit_multiplier_per_feed: Option<u16>,
pub asset_stream_multiplier_per_asset: Option<u16>,
pub use_solana_epochs: Option<bool>,
pub custom_epoch_length_seconds: Option<u64>,
}Validation:
- Signer must be state.authority or in state.admin_list
- Multipliers must be reasonable (non-zero, not excessive)
- If updating delay multipliers, must satisfy:
delay_min_multiplier < delay_max_multiplier - Changing delay pricing parameters will affect cost calculations for all active subscriptions on their next extension
Instruction: subscriber_state_manage_admins
Purpose: Add/remove admin accounts
Accounts:
state(mut) - SubscriberState PDAauthority(signer) - Must be primary authority
Parameters:
pub struct SubscriberStateManageAdminsParams {
pub add_admins: Vec<Pubkey>,
pub remove_admins: Vec<Pubkey>,
}Validation:
- Only state.authority can modify admins
- Cannot exceed max admin count (8)
Instruction: subscriber_state_manage_overrides
Purpose: Add/remove override accounts with permanent free access
Accounts:
state(mut) - SubscriberState PDAauthority(signer) - Must be authority or admin
Parameters:
pub struct SubscriberStateManageOverridesParams {
pub add_overrides: Vec<Pubkey>,
pub remove_overrides: Vec<Pubkey>,
}Validation:
- Signer must be state.authority or in state.admin_list
- Cannot exceed max override count (32)
Instruction: subscriber_state_manage_tokens
Purpose: Add, update, or disable accepted payment tokens
Accounts:
state(mut) - SubscriberState PDAauthority(signer) - Must be authority or admin
Parameters:
pub struct SubscriberStateManageTokensParams {
pub add_tokens: Vec<AcceptedTokenConfig>,
pub update_tokens: Vec<TokenUpdate>,
pub disable_tokens: Vec<Pubkey>, // Mint addresses to disable
}
pub struct TokenUpdate {
pub mint: Pubkey,
pub new_burn_vault: Option<Pubkey>,
pub new_time_multiplier: Option<u16>,
}Validation:
- Signer must be state.authority or in state.admin_list
- Cannot exceed max token count (16)
- For updates, token must already exist
- time_multiplier must be reasonable (> 0, < 1000000)
- Burn vaults must be valid token accounts
Logic:
- Add new tokens to available slots
- Update existing token configurations
- Disable tokens by setting enabled = 0 (don't remove to preserve historical data)
Instruction: subscription_init
Purpose: Create a new subscription account for a user
Accounts:
subscription(init, mut) - Subscription PDA for userowner(signer) - User walletstate- SubscriberState PDA (for validation)payer(signer, mut) - Pays for account creationsystem_program
Parameters:
pub struct SubscriptionInitParams {}Validation:
- Owner must sign
- Subscription account must not exist
Logic:
- Initialize account with default/zero values:
owner= signer's pubkeyauthorized_users= all default pubkeysauthorized_user_count= 0delay_ms= 5000 (5 second delay, free tier)oracle_requests_per_minute= 0crossbar_requests_per_minute= 0unique_feed_limit= 0asset_stream_limit= 0subscription_start_epoch= 0subscription_end_epoch= 0- All other fields zeroed
- User must call
subscription_set_paramsto configure tier - User can call
subscription_manage_usersto add team members - User must call
subscription_extendto activate with payment
Instruction: subscription_set_params
Purpose: Configure or update subscription tier settings
Accounts:
subscription(mut) - User's Subscription PDAowner(signer) - Must be subscription ownerstate- SubscriberState PDA (for validation)
Parameters:
pub struct SubscriptionSetParamsParams {
pub delay_ms: u16, // Data delay in milliseconds (0-60000)
pub oracle_requests_per_minute: u16, // Max oracle requests per minute (0-1000)
pub crossbar_requests_per_minute: u16, // Max crossbar requests per minute (0-1000)
pub unique_feed_limit: u16, // Number of unique feeds
pub asset_stream_limit: u16, // Number of assets to stream
}Validation:
- Owner must sign
- Subscription must exist
- Parameters must be within valid ranges:
- delay_ms: 0-60000 (0ms to 60s)
- oracle_requests_per_minute: 0-1000
- crossbar_requests_per_minute: 0-1000
- unique_feed_limit: >= 0
- asset_stream_limit: >= 0
Logic:
- Update subscription tier settings
- If subscription is active (subscription_end_epoch >= current_epoch), recalculate remaining epochs:
- Calculate old tier cost per epoch using old settings
- Calculate remaining epochs:
remaining = subscription_end_epoch - current_epoch - Calculate remaining value:
value = remaining * old_cost_per_epoch - Calculate new tier cost per epoch using new settings
- Calculate new remaining epochs:
new_remaining = value / new_cost_per_epoch(rounded down) - Update
subscription_end_epoch = current_epoch + new_remaining - Important: If new_remaining rounds down to 0, subscription becomes expired
- If subscription is expired, just update settings (no recalculation needed)
- Can be called multiple times to adjust settings
- Emits
SubscriptionParamsSetEvent
Note: This pro-rates the remaining subscription value. Upgrading to a more expensive tier will reduce remaining epochs; downgrading to a cheaper tier will increase remaining epochs. This preserves economic fairness.
Instruction: subscription_manage_users
Purpose: Add or remove authorized users who can access the subscription
Accounts:
subscription(mut) - User's Subscription PDAowner(signer) - Must be subscription ownerstate- SubscriberState PDA (for validation)
Parameters:
pub struct SubscriptionManageUsersParams {
pub add_users: Vec<Pubkey>, // Users to authorize
pub remove_users: Vec<Pubkey>, // Users to revoke access
}Validation:
- Owner must sign (only owner can manage authorized users)
- Cannot exceed max authorized users (16)
- Cannot add owner to authorized_users list (owner already has access)
- Cannot add duplicate users
Logic:
- Add new users to authorized_users array
- Remove specified users from authorized_users array
- Update authorized_user_count
- Emits
SubscriptionUsersUpdatedEvent
Note: Authorized users can use the subscription for API access but cannot:
- Manage other authorized users (add/remove users)
- Change subscription settings (tier configuration)
- Close the subscription
- However, authorized users can extend the subscription (anyone can pay to extend any subscription)
Instruction: subscription_extend
Purpose: Burn payment tokens to extend subscription duration
Accounts:
subscription(mut) - Subscription PDA to extendstate- SubscriberState PDApayer(signer) - Anyone who wants to pay for the subscriptionpayer_token_account(mut) - Payer's payment token accountburn_vault(mut) - Token-specific burn vault from statepayment_mint- Payment token minttoken_program- SPL Token program
Parameters:
pub struct SubscriptionExtendParams {
pub token_amount: u64, // Amount of token lamports to burn
pub admin_extend: bool, // If true and payer is admin, skip token burning
}Validation:
- Payer must sign (can be anyone, not just owner)
- Subscription must be initialized
- If
admin_extend = true:- Payer must be state.authority OR in state.admin_list
- Token transfer will be skipped
- If
admin_extend = false:- Payer must have sufficient token balance
- Payment token must be in state.accepted_tokens and enabled
- payment_mint must match one of the configured tokens
- burn_vault must match the vault configured for that token
Logic:
- Check if subscription owner is in override list (if so, skip payment and set permanent access)
- If
admin_extend = true:- Verify payer is state.authority or in state.admin_list (else error)
- Skip token transfer entirely
- Use default time_multiplier of 10000 (1x)
- Continue to step 5
- Verify payment_mint is in accepted_tokens list and enabled
- Get token's time_multiplier from state
- Calculate current epoch
- Calculate base cost per epoch based on subscription settings:
base_cost_per_epoch = base_price_per_epoch * delay_multiplier * (1 + oracle_requests_per_minute * oracle_request_multiplier_per_req) * (1 + crossbar_requests_per_minute * crossbar_request_multiplier_per_req) * (1 + unique_feed_limit * feed_limit_multiplier_per_feed) * (1 + asset_stream_limit * asset_stream_multiplier_per_asset) - Calculate effective epochs:
- If
admin_extend = true:// Admin extends directly by token_amount epochs (no cost calculation) effective_epochs = token_amount - If
admin_extend = false:// First calculate how many "base epochs" the tokens buy base_epochs = token_amount / base_cost_per_epoch // Then apply the token's time multiplier effective_epochs = base_epochs * (time_multiplier / 10000) // Example: If time_multiplier = 20000 (2x), paying for 1 epoch gives 2 epochs // If time_multiplier = 5000 (0.5x), paying for 2 epochs gives 1 epoch
- If
- If subscription is currently active (subscription_end_epoch >= current_epoch):
new_end_epoch = subscription_end_epoch + effective_epochs
- If subscription is expired (subscription_end_epoch < current_epoch):
new_end_epoch = current_epoch + effective_epochs- Update
subscription_start_epoch = current_epoch
- If
admin_extend = false: Transfer tokens from payer to burn_vault - Update subscription:
subscription_end_epoch = new_end_epoch(rounded down)total_tokens_burnt += token_amount(only if not admin_extend)last_burn_amount = token_amount(or 0 if admin_extend)last_payment_token = payment_mint(or default if admin_extend)last_extended_slot = current_slot
Note: Allowing anyone to extend subscriptions enables several use cases:
- Gifting: Friends can pay for each other's subscriptions
- Sponsorship: Projects can sponsor developer subscriptions
- Corporate billing: Company treasury can pay for employee subscriptions
- Scholarships: DAOs can fund subscriptions for students/researchers
- Automated renewal services: Third-party services can manage renewals
- Admin Extensions: Program admins can extend without token burning for:
- Testing and debugging
- Emergency extensions
- Promotional campaigns
- Customer support cases
- Community grants
Instruction: subscription_close
Purpose: Close subscription account and reclaim rent
Accounts:
subscription(mut) - User's Subscription PDAowner(signer) - Must be subscription ownerreceiver(mut) - Receives reclaimed SOL
Parameters:
pub struct SubscriptionCloseParams {}Validation:
- Owner must sign
Logic:
- Transfer remaining lamports to receiver
- Close subscription account
The subscription cost calculation involves two steps: calculating the base cost per epoch, then applying the token's time multiplier to determine effective subscription duration.
The pricing system is built around a configurable baseline cost (base_price_per_epoch) that represents the minimum cost per epoch for any subscription. This baseline is then multiplied by various tier-specific multipliers to calculate the final cost:
base_price_per_epoch: The baseline cost in lamports per epoch (normalized to SWTCH tokens)- This is the foundation of all pricing calculations
- Configured during state initialization via
subscriber_state_init - Can be updated by admins via
subscriber_state_set_config - Example: 1,000,000,000 lamports (1 SWTCH)
All other costs are calculated as additive multipliers on top of this baseline:
- Delay multipliers (linear curve): Based on the
delay_msvalue in the subscription, calculated using a linear pricing curve:delay_multiplier = delay_max_multiplier - (delay_ms * delay_multiplier_slope) delay_multiplier = clamp(delay_multiplier, delay_min_multiplier, delay_max_multiplier)delay_max_multiplier: Price at 0ms delay (no delay, most expensive, e.g., 100x = 1000000)delay_min_multiplier: Minimum price at maximum delay (cheapest, e.g., 1x = 10000)delay_multiplier_slope: Rate of price decrease per millisecond of delay (e.g., 10 = 0.001x per ms)- Example: With max=100000, min=10000, slope=18:
- 0ms delay → 100000 (10x)
- 1000ms delay → 82000 (8.2x)
- 5000ms delay → 10000 (1x, clamped to minimum)
- Oracle request costs (per request/minute)
- Crossbar request costs (per request/minute)
- Feed limit costs (per unique feed)
- Asset stream costs (per asset)
This architecture allows admins to easily adjust the overall pricing by modifying the baseline, or fine-tune specific features by adjusting their multipliers. The flexible delay_ms field with a linear pricing curve provides smooth pricing transitions based on the exact delay value, avoiding pricing discontinuities.
The system distinguishes between two types of API requests, each with independent rate limits and cost multipliers:
Oracle Requests (oracle_requests_per_minute):
- On-chain oracle data queries
- Examples: Reading aggregator data, fetching feed values, querying oracle states
- Generally more expensive due to on-chain data access costs
- Cost multiplier:
oracle_request_multiplier_per_req
Crossbar Requests (crossbar_requests_per_minute):
- Off-chain service queries (Switchboard's Crossbar service)
- Examples: Job simulations, task scheduling, off-chain data processing
- Different resource usage profile than oracle queries
- Cost multiplier:
crossbar_request_multiplier_per_req
Why Separate Limits?
- Different backend infrastructure costs
- Allows pricing flexibility (e.g., cheaper crossbar for high-volume simulation workloads)
- Better capacity planning and resource allocation
- Users can optimize costs based on their usage patterns
fn calculate_base_cost_per_epoch(state: &SubscriberState, subscription: &Subscription) -> u64 {
let base = state.base_price_per_epoch;
// Calculate delay multiplier using linear curve
// Formula: delay_mult = max - (delay_ms * slope), clamped to [min, max]
let delay_mult = {
let reduction = (subscription.delay_ms as u64)
.checked_mul(state.delay_multiplier_slope as u64)
.unwrap_or(0);
let calculated = (state.delay_max_multiplier as u64)
.saturating_sub(reduction);
// Clamp to minimum
let clamped = calculated.max(state.delay_min_multiplier as u64);
// Ensure we don't exceed maximum
clamped.min(state.delay_max_multiplier as u64)
};
// Calculate additional costs for request rates
let oracle_mult = 10000 + (subscription.oracle_requests_per_minute as u64
* state.oracle_request_multiplier_per_req as u64);
let crossbar_mult = 10000 + (subscription.crossbar_requests_per_minute as u64
* state.crossbar_request_multiplier_per_req as u64);
// Calculate additional costs for feeds and streams
let feed_mult = 10000 + (subscription.unique_feed_limit as u64
* state.feed_limit_multiplier_per_feed as u64);
let asset_mult = 10000 + (subscription.asset_stream_limit as u64
* state.asset_stream_multiplier_per_asset as u64);
// Combine all multipliers (basis points, so divide by 10000)
let total_cost = base
.checked_mul(delay_mult).unwrap()
.checked_div(10000).unwrap()
.checked_mul(oracle_mult).unwrap()
.checked_div(10000).unwrap()
.checked_mul(crossbar_mult).unwrap()
.checked_div(10000).unwrap()
.checked_mul(feed_mult).unwrap()
.checked_div(10000).unwrap()
.checked_mul(asset_mult).unwrap()
.checked_div(10000).unwrap();
total_cost
}fn calculate_effective_epochs(
base_cost_per_epoch: u64,
token_amount: u64,
token_time_multiplier: u16,
) -> u64 {
// Calculate how many base epochs the token amount buys
let base_epochs = token_amount
.checked_div(base_cost_per_epoch)
.unwrap_or(0);
// Apply the token's time multiplier
// time_multiplier is in basis points (10000 = 1x)
let effective_epochs = base_epochs
.checked_mul(token_time_multiplier as u64).unwrap()
.checked_div(10000).unwrap();
effective_epochs
}-
time_multiplier = 10000 (1x): Standard rate (e.g., SWTCH token)
- Pay 100 tokens → Get 1 epoch → 1 epoch duration
-
time_multiplier = 20000 (2x): Double duration (e.g., premium token)
- Pay 100 tokens → Get 1 epoch → 2 epochs duration
-
time_multiplier = 5000 (0.5x): Half duration (e.g., discounted token)
- Pay 200 tokens → Get 2 epochs → 1 epoch duration
-
time_multiplier = 50000 (5x): 5x duration (e.g., highly valued token)
- Pay 100 tokens → Get 1 epoch → 5 epochs duration
The program supports two epoch modes:
- Uses native Solana epoch boundaries
- More decentralized and trustless
- Epochs are approximately 2-3 days
- Uses slot-based time calculation
- More predictable duration
- Configured via
custom_epoch_length_seconds
fn get_current_epoch(state: &SubscriberState, clock: &Clock) -> u64 {
if state.use_solana_epochs == 1 {
clock.epoch
} else {
let seconds_per_slot = 400; // ~400ms per slot
let slots_per_epoch = (state.custom_epoch_length_seconds * 1000) / seconds_per_slot;
clock.slot / slots_per_epoch
}
}subscriber_state_set_config: authority OR admin_listsubscriber_state_manage_admins: authority ONLYsubscriber_state_manage_overrides: authority OR admin_listsubscriber_state_manage_tokens: authority OR admin_listsubscription_extend(with admin_extend=true): authority OR admin_list (extends without burning tokens)
subscription_init: Any user (creates their own subscription)subscription_set_params: Subscription owner ONLYsubscription_manage_users: Subscription owner ONLYsubscription_extend: Anyone (can pay to extend any subscription - enables gifting/sponsorship)subscription_close: Subscription owner ONLY
- Owner: Full control over subscription (settings, user management, closing)
- Authorized Users (up to 16): Can access subscription benefits (API usage) but have limited management rights:
- ✅ Can use API with subscription benefits
- ✅ Can extend subscription (pay to add more epochs)
- ❌ Cannot modify subscription settings
- ❌ Cannot add/remove other users
- ❌ Cannot close subscription
- Anyone (including non-users): Can extend any subscription (enables gifting/sponsorship)
- Oracle backend service should check:
signer == owner OR signer in authorized_users
- Accounts in
override_listhave permanent free access - When extending subscription, override accounts skip payment and get permanent access
- Override accounts still need to:
- Call
subscription_initto create their subscription account - Call
subscription_set_paramsto configure desired tier settings - Can optionally call
subscription_extend(will skip payment due to override status)
- Call
#[event]
pub struct SubscriptionExtendedEvent {
pub subscription_owner: Pubkey, // Owner of the subscription being extended
pub payer: Pubkey, // Who paid for the extension (can be different from owner)
pub payment_token: Pubkey, // Token mint used for payment (default if admin_extend)
pub tokens_burnt: u64, // Amount of tokens burnt (0 if admin_extend)
pub base_epochs_purchased: u64, // Base epochs before multiplier
pub effective_epochs_added: u64, // Actual epochs added after multiplier
pub time_multiplier: u16, // Token's time multiplier used
pub admin_extend: bool, // Whether this was an admin extension (no tokens burnt)
pub new_end_epoch: u64,
pub current_epoch: u64,
}
#[event]
pub struct SubscriptionInitializedEvent {
pub owner: Pubkey,
}
#[event]
pub struct SubscriptionParamsSetEvent {
pub owner: Pubkey,
pub delay_ms: u16,
pub oracle_requests_per_minute: u16,
pub crossbar_requests_per_minute: u16,
pub unique_feed_limit: u16,
pub asset_stream_limit: u16,
pub old_end_epoch: u64, // End epoch before update
pub new_end_epoch: u64, // End epoch after pro-rating (if active)
pub was_active: bool, // Whether subscription was active during update
}
#[event]
pub struct StateConfigUpdatedEvent {
pub authority: Pubkey,
pub base_price_per_epoch: u64,
}
#[event]
pub struct TokenConfiguredEvent {
pub authority: Pubkey,
pub token_mint: Pubkey,
pub burn_vault: Pubkey,
pub time_multiplier: u16,
pub action: u8, // 0 = added, 1 = updated, 2 = disabled
}
#[event]
pub struct SubscriptionUsersUpdatedEvent {
pub owner: Pubkey,
pub added_users: Vec<Pubkey>,
pub removed_users: Vec<Pubkey>,
pub total_authorized_users: u8,
}#[error_code]
pub enum SubscriberError {
#[msg("Invalid admin authority")]
InvalidAdmin,
#[msg("Admin list is full")]
AdminListFull,
#[msg("Override list is full")]
OverrideListFull,
#[msg("Accepted token list is full")]
TokenListFull,
#[msg("Invalid delay value, must be 0-60000 ms")]
InvalidDelay,
#[msg("Invalid rate limit, must be 0-1000")]
InvalidRateLimit,
#[msg("Insufficient token balance")]
InsufficientBalance,
#[msg("Invalid subscription owner")]
InvalidOwner,
#[msg("Subscription already expired")]
SubscriptionExpired,
#[msg("Invalid multiplier value")]
InvalidMultiplier,
#[msg("Invalid time multiplier value")]
InvalidTimeMultiplier,
#[msg("Arithmetic overflow in cost calculation")]
ArithmeticOverflow,
#[msg("Invalid token mint")]
InvalidMint,
#[msg("Invalid burn vault")]
InvalidBurnVault,
#[msg("Payment token not accepted")]
TokenNotAccepted,
#[msg("Payment token is disabled")]
TokenDisabled,
#[msg("Token not found in accepted list")]
TokenNotFound,
#[msg("Burn vault mismatch for payment token")]
BurnVaultMismatch,
#[msg("Authorized users list is full")]
AuthorizedUsersListFull,
#[msg("User not authorized for this subscription")]
UserNotAuthorized,
#[msg("Cannot add owner to authorized users list")]
CannotAddOwner,
#[msg("User already in authorized list")]
UserAlreadyAuthorized,
#[msg("Admin extend flag set but payer is not an admin")]
NotAuthorizedAsAdmin,
}The oracle backend service should:
Client-Side Flow:
// Client signs a message every 5 minutes
const message = JSON.stringify({
timestamp: Date.now(),
wallet: publicKey.toString(),
action: "authenticate"
});
const signature = await wallet.signMessage(new TextEncoder().encode(message));
// Send with each request or via WebSocket heartbeat
await fetch("/api/data", {
headers: {
"X-Wallet-Pubkey": publicKey.toString(),
"X-Wallet-Signature": bs58.encode(signature),
"X-Message": message,
}
});Server-Side Session Management:
- Session Creation: On first valid signature, create session
- Session Renewal: Client signs new message every 5 minutes
- Session Expiration: Clear session if no signature received in 60 minutes
- Benefits:
- Continuous proof of wallet ownership
- Prevents session hijacking
- No need for separate session tokens
- Aligns with Web3 principles
- Can detect wallet disconnect
Session Cache Structure:
struct TierSettings {
delay_ms: u16,
oracle_requests_per_minute: u16,
crossbar_requests_per_minute: u16,
unique_feed_limit: u16,
asset_stream_limit: u16,
}
struct UserSession {
wallet: Pubkey,
subscription_pda: Pubkey,
subscription_owner: Pubkey, // Cached from on-chain
is_owner: bool, // True if wallet is subscription owner
tier_settings: TierSettings, // Cached from on-chain
subscription_end_epoch: u64, // Cached from on-chain
last_signature_time: u64, // Unix timestamp of last valid signature
session_created_at: u64, // Unix timestamp
active_streams: u8, // Current open connections
oracle_request_count: u32, // Oracle requests in current minute window
crossbar_request_count: u32, // Crossbar requests in current minute window
}Cache Invalidation Events:
- Listen to
SubscriptionExtendedEvent→ Update subscription_end_epoch - Listen to
SubscriptionParamsSetEvent→ Update tier_settings - Listen to
SubscriptionUsersUpdatedEvent→ Refresh authorization check - TTL: 1 epoch (fallback if events missed)
On Each API Request:
async fn validate_request(
wallet: Pubkey,
signature: Signature,
message: &[u8],
) -> Result<UserSession> {
// 1. Verify signature
if !signature.verify(&wallet, message) {
return Err("Invalid signature");
}
// 2. Check message timestamp (must be within 10 minutes)
let msg_timestamp = parse_timestamp(message)?;
let now = current_timestamp();
if now - msg_timestamp > 600 { // 10 minutes
return Err("Message expired");
}
// 3. Check session cache
if let Some(session) = session_cache.get(&wallet) {
// Session exists, check if signature is fresh
if now - session.last_signature_time < 3600 { // 1 hour
// Update last signature time
session.last_signature_time = now;
return Ok(session);
} else {
// Session expired, remove and re-validate on-chain
session_cache.remove(&wallet);
}
}
// 4. No valid session, validate on-chain
let session = validate_subscription_onchain(wallet).await?;
// 5. Cache the session
session_cache.insert(wallet, session.clone());
Ok(session)
}When cache miss or session expired:
async fn validate_subscription_onchain(wallet: Pubkey) -> Result<UserSession> {
// 1. Try to find subscription owned by wallet
let subscription_pda = derive_subscription_pda(&wallet);
if let Ok(subscription) = fetch_account::<Subscription>(&subscription_pda) {
// Wallet owns a subscription
let current_epoch = get_current_epoch();
if subscription.subscription_end_epoch >= current_epoch {
return Ok(UserSession {
wallet,
subscription_pda,
subscription_owner: subscription.owner,
is_owner: true,
tier_settings: subscription.tier_settings(),
subscription_end_epoch: subscription.subscription_end_epoch,
last_signature_time: current_timestamp(),
session_created_at: current_timestamp(),
active_streams: 0,
request_count: 0,
});
}
}
// 2. Check if wallet is authorized user on another subscription
// Note: This requires indexing or having client provide subscription_pda
// Recommended: Client includes "subscription_pda" in signed message
let subscription_pda = get_subscription_from_message()?;
let subscription = fetch_account::<Subscription>(&subscription_pda)?;
// Check if wallet is in authorized_users
let is_authorized = subscription.authorized_users[..subscription.authorized_user_count]
.contains(&wallet);
if !is_authorized {
return Err("Wallet not authorized for this subscription");
}
let current_epoch = get_current_epoch();
if subscription.subscription_end_epoch < current_epoch {
return Err("Subscription expired");
}
// 3. Check override list
let state = fetch_account::<SubscriberState>(&STATE_PDA)?;
if state.override_list[..state.override_count].contains(&wallet) {
// Permanent access
return Ok(UserSession {
wallet,
subscription_pda,
subscription_owner: subscription.owner,
is_owner: false,
tier_settings: TierSettings::max(), // Max tier for overrides
subscription_end_epoch: u64::MAX, // Never expires
last_signature_time: current_timestamp(),
session_created_at: current_timestamp(),
active_streams: 0,
request_count: 0,
});
}
Ok(UserSession {
wallet,
subscription_pda,
subscription_owner: subscription.owner,
is_owner: false,
tier_settings: subscription.tier_settings(),
subscription_end_epoch: subscription.subscription_end_epoch,
last_signature_time: current_timestamp(),
session_created_at: current_timestamp(),
active_streams: 0,
request_count: 0,
})
}Rate Limiting (Per Subscription, Separate for Oracle and Crossbar):
// Determine request type based on endpoint
enum RequestType {
Oracle, // On-chain oracle data queries
Crossbar, // Off-chain crossbar API queries
}
async fn check_rate_limit(
session: &UserSession,
request_type: RequestType,
) -> Result<()> {
let (rate_key, limit) = match request_type {
RequestType::Oracle => (
format!("ratelimit:oracle:{}:{}", session.subscription_pda, current_minute()),
session.tier_settings.oracle_requests_per_minute,
),
RequestType::Crossbar => (
format!("ratelimit:crossbar:{}:{}", session.subscription_pda, current_minute()),
session.tier_settings.crossbar_requests_per_minute,
),
};
let count = redis.incr(&rate_key).await?;
redis.expire(&rate_key, 60).await?;
if count > limit {
return Err(format!("{:?} rate limit exceeded for subscription", request_type));
}
Ok(())
}Endpoint Classification:
// Map endpoints to request types
fn classify_request(path: &str) -> RequestType {
match path {
// Oracle endpoints - on-chain data queries
path if path.starts_with("/api/oracle/") => RequestType::Oracle,
path if path.starts_with("/v1/aggregator/") => RequestType::Oracle,
path if path.starts_with("/v1/feed/") => RequestType::Oracle,
// Crossbar endpoints - off-chain service queries
path if path.starts_with("/api/crossbar/") => RequestType::Crossbar,
path if path.starts_with("/v1/simulator/") => RequestType::Crossbar,
path if path.starts_with("/v1/job/") => RequestType::Crossbar,
// Default to crossbar for unknown endpoints (more restrictive)
_ => RequestType::Crossbar,
}
}Feed & Stream Limits:
async fn validate_request_limits(
session: &UserSession,
requested_feeds: &[String],
is_stream: bool,
) -> Result<()> {
// Check unique feed limit
if requested_feeds.len() > session.tier_settings.unique_feed_limit {
return Err("Too many feeds requested");
}
// Check concurrent stream limit
if is_stream {
let active_streams = get_active_streams(session.subscription_pda).await?;
if active_streams >= session.tier_settings.asset_stream_limit {
return Err("Stream limit reached");
}
}
Ok(())
}Data Delay:
fn apply_data_delay(
session: &UserSession,
data: &mut PriceData,
) {
let delay_seconds = session.tier_settings.delay_ms / 1000;
// Only return data older than delay_seconds
data.filter_by_age(delay_seconds);
}Shared Limits Across All Users:
// All limits are keyed by subscription_pda, not wallet
// This means owner + 16 authorized users share:
// - Same oracle_requests_per_minute counter
// - Same crossbar_requests_per_minute counter
// - Same unique_feed_limit pool
// - Same asset_stream_limit pool
// Example: Rate limiting
let oracle_key = format!("ratelimit:oracle:{}", session.subscription_pda);
let crossbar_key = format!("ratelimit:crossbar:{}", session.subscription_pda);
// NOT: format!("ratelimit:{}", session.wallet)
// If 17 users each make 10 oracle requests/min = 170 requests/min total
// If tier allows 100 oracle req/min, they'll hit limit quickly
// But crossbar requests have separate counter (e.g., 200 req/min)For Streaming Connections:
// Client sends signed message every 5 minutes over WebSocket
ws.on_message(|msg| {
match msg {
Message::Auth { signature, message } => {
validate_signature(signature, message)?;
update_session_timestamp(wallet)?;
},
Message::Subscribe { feeds } => {
check_stream_limits(session)?;
increment_active_streams(session.subscription_pda)?;
},
Message::Unsubscribe => {
decrement_active_streams(session.subscription_pda)?;
}
}
});
// Background task: Close connections with expired sessions
tokio::spawn(async move {
loop {
sleep(Duration::from_secs(60)).await;
close_expired_sessions().await;
}
});Client Signs:
{
"wallet": "5Gn8Y...",
"subscription_pda": "7Hj9K...", // Optional: speeds up lookup
"timestamp": 1704067200000,
"nonce": "random_uuid", // Prevents replay attacks
"action": "authenticate"
}Benefits:
- Timestamp prevents old signatures
- Nonce prevents replay attacks
- Subscription PDA hint speeds up validation
- Can include additional metadata
Automatic Cleanup:
- Remove sessions if no signature in 60 minutes
- Clear rate limit counters after 1 minute
- Close WebSocket connections after 60 minutes idle
- Clean up stream counters on disconnect
Graceful Degradation:
- If on-chain RPC fails, continue with cached data
- Set conservative TTL (1 epoch)
- Background refresh every N minutes
- Alert on RPC errors
Assuming:
base_price_per_epoch = 1_000_000_000(1 SWTCH = 1B lamports)- Delay pricing curve:
delay_max_multiplier = 100000(10x at 0ms delay)delay_min_multiplier = 10000(1x minimum)delay_multiplier_slope = 18(0.0018x decrease per ms)- Results:
- 0ms → 100000 (10x)
- 1000ms → 82000 (8.2x)
- 2000ms → 64000 (6.4x)
- 3000ms → 46000 (4.6x)
- 4000ms → 28000 (2.8x)
- 5000ms+ → 10000 (1x, clamped to min)
oracle_request_multiplier_per_req = 100(0.01x per oracle request/min)crossbar_request_multiplier_per_req = 50(0.005x per crossbar request/min)feed_limit_multiplier_per_feed = 500(0.05x per feed)asset_stream_multiplier_per_asset = 2000(0.2x per asset)
Accepted tokens:
- SWTCH: time_multiplier = 10000 (1x, standard)
- USDC: time_multiplier = 5000 (0.5x, costs twice as many USDC)
- PREMIUM_TOKEN: time_multiplier = 20000 (2x, premium token gives 2x duration)
- Delay: 5000ms (5s delay)
- Oracle requests: 10 req/min
- Crossbar requests: 20 req/min
- Feed limit: 5 feeds
- Asset streams: 10 assets
- Payment: 45 SWTCH
Calculation:
- Delay multiplier = 100000 - (5000 * 18) = 10000 (clamped to min, 1x)
- Base cost per epoch = 1 SWTCH * 1.0 (delay) * 1.10 (oracle) * 1.10 (crossbar) * 1.25 (feeds) * 3.0 (assets)
- Base cost per epoch = 1 * 1.0 * 1.10 * 1.10 * 1.25 * 3.0 = 4.5375 SWTCH
- Base epochs = 45 SWTCH / 4.5375 SWTCH ≈ 9.92 epochs (rounds to 9)
- SWTCH time_multiplier = 1x
- Effective duration = 9 epochs
- Same settings as Scenario 1
- Payment: 90 USDC (equivalent value)
Calculation:
- Base cost per epoch = 4.5375 SWTCH equivalent
- Base epochs = 90 USDC / 4.5375 ≈ 19.83 epochs (rounds to 19)
- USDC time_multiplier = 0.5x
- Effective duration = 19 * 0.5 = 9.5 epochs (rounds to 9) (same as SWTCH but costs 2x as much)
- Same settings as Scenario 1
- Payment: 45 PREMIUM_TOKEN
Calculation:
- Base cost per epoch = 4.5375 tokens
- Base epochs = 45 / 4.5375 ≈ 9.92 epochs (rounds to 9)
- PREMIUM_TOKEN time_multiplier = 2x
- Effective duration = 9 * 2 = 18 epochs (double duration!)
- Delay: 0ms (no delay)
- Oracle requests: 100 req/min
- Crossbar requests: 200 req/min
- Feed limit: 50 feeds
- Asset streams: 100 assets
- Payment: 13,230 SWTCH
Calculation:
- Delay multiplier = 100000 - (0 * 18) = 100000 (10x, max)
- Base cost per epoch = 1 SWTCH * 10.0 (delay) * 11.0 (oracle) * 11.0 (crossbar) * 3.5 (feeds) * 21.0 (assets)
- Base cost per epoch = 1 * 10.0 * 11.0 * 11.0 * 3.5 * 21.0 = 88,935 SWTCH
- Base epochs = 13,230 / 88,935 ≈ 0.148 epochs (rounds to 0)
- Note: At these settings, need approximately 88,935 SWTCH for 1 epoch
- For 10 epochs with these settings, need ~889,350 SWTCH
- SWTCH time_multiplier = 1x
- Effective duration = 0 epochs (insufficient payment)
Demonstrates smooth pricing curve at different delay values:
Settings (constant across all):
- Oracle requests: 10 req/min
- Crossbar requests: 20 req/min
- Feed limit: 5 feeds
- Asset streams: 10 assets
- Payment: 45 SWTCH
Delay: 0ms (no delay)
- Delay multiplier = 100000 (10x)
- Cost per epoch = 1 * 10.0 * 1.10 * 1.10 * 1.25 * 3.0 = 45.375 SWTCH
- Duration = 0.99 epochs (rounds to 0)
Delay: 1000ms (1s delay)
- Delay multiplier = 100000 - (1000 * 18) = 82000 (8.2x)
- Cost per epoch = 1 * 8.2 * 1.10 * 1.10 * 1.25 * 3.0 = 37.2075 SWTCH
- Duration = 1.2 epochs (rounds to 1)
Delay: 2000ms (2s delay)
- Delay multiplier = 100000 - (2000 * 18) = 64000 (6.4x)
- Cost per epoch = 1 * 6.4 * 1.10 * 1.10 * 1.25 * 3.0 = 29.04 SWTCH
- Duration = 1.5 epochs (rounds to 1)
Delay: 3000ms (3s delay)
- Delay multiplier = 100000 - (3000 * 18) = 46000 (4.6x)
- Cost per epoch = 1 * 4.6 * 1.10 * 1.10 * 1.25 * 3.0 = 20.8725 SWTCH
- Duration = 2.15 epochs (rounds to 2)
Delay: 5000ms (5s delay)
- Delay multiplier = 100000 - (5000 * 18) = 10000 (1x, clamped to min)
- Cost per epoch = 1 * 1.0 * 1.10 * 1.10 * 1.25 * 3.0 = 4.5375 SWTCH
- Duration = 9.92 epochs (rounds to 9)
Key Observation: The linear curve provides smooth price transitions. Users can optimize costs by accepting slightly more delay (e.g., 3s instead of 0s saves ~75% on cost).
User starts with basic tier and upgrades to premium:
Initial State:
- Tier: Delay 5000ms, 10 oracle req/min, 20 crossbar req/min, 5 feeds, 10 assets
- Cost: 4.5375 SWTCH per epoch
- Paid: 45 SWTCH
- Got: 9 epochs
- After 3 epochs, 6 epochs remaining
User calls subscription_set_params to upgrade:
- New Tier: Delay 0ms, 100 oracle req/min, 200 crossbar req/min, 50 feeds, 100 assets
- New Cost: 88,935 SWTCH per epoch
Recalculation:
- Old remaining value: 6 epochs * 4.5375 SWTCH = 27.225 SWTCH worth
- New remaining epochs: 27.225 / 88,935 = 0.00030 epochs
- Rounds down to 0 epochs - subscription expires!
- User must call
subscription_extendto pay for premium tier
User starts with premium tier and downgrades:
Initial State:
- Tier: Delay 0ms, 100 oracle req/min, 200 crossbar req/min, 50 feeds, 100 assets
- Cost: 88,935 SWTCH per epoch
- Paid: 177,870 SWTCH
- Got: 2 epochs
- After 0 epochs (just started), 2 epochs remaining
User calls subscription_set_params to downgrade:
- New Tier: Delay 5000ms, 10 oracle req/min, 20 crossbar req/min, 5 feeds, 10 assets
- New Cost: 4.5375 SWTCH per epoch
Recalculation:
- Old remaining value: 2 epochs * 88,935 SWTCH = 177,870 SWTCH worth
- New remaining epochs: 177,870 / 4.5375 = 39,201 epochs
- Subscription extended to 39,201 epochs at the lower tier!
A company wants to provide oracle access to their development team:
Setup:
- Company owner (Alice) calls
subscription_init - Alice calls
subscription_set_paramswith premium tier:- Delay: 0ms (no delay)
- Oracle requests: 100 req/min
- Crossbar requests: 200 req/min
- Feed limit: 50 feeds
- Asset streams: 100 assets
- Alice calls
subscription_extendand pays for 30 epochs - Alice calls
subscription_manage_usersto add 5 developers:- Bob, Carol, Dave, Eve, Frank
Usage:
- Alice (owner): Can use API, manage subscription, add/remove users, pay for extensions
- Bob-Frank (authorized users): Can use API with same tier benefits
- All 6 users share the subscription limits:
- Combined they can make 100 oracle requests/min
- Combined they can make 200 crossbar requests/min
- Combined they can access 50 unique feeds
- Combined they can stream 100 assets
Later, Alice adds 10 more team members:
- Now 16 users total (Alice + 15 authorized)
- All 16 still share the same 100 oracle req/min and 200 crossbar req/min limits
- If they need more capacity, Alice must upgrade tier
When Bob leaves the company:
- Alice calls
subscription_manage_usersto remove Bob - Bob immediately loses access
- Remaining 15 users still have access
Scenario A: Friend Gifting
- Bob has a subscription that's about to expire
- Alice (Bob's friend) wants to gift him a subscription extension
- Alice calls
subscription_extendfor Bob's subscription PDA - Alice burns 450 SWTCH from her wallet
- Bob's subscription is extended by 100 epochs
- Bob didn't need to do anything or spend his own tokens
Scenario B: Project Sponsorship
- Switchboard DAO wants to sponsor subscriptions for 10 promising developers
- DAO treasury has a multisig wallet with SWTCH tokens
- For each developer:
- Developer calls
subscription_initto create their account - Developer calls
subscription_set_paramsto configure their tier - DAO calls
subscription_extendon each developer's subscription - DAO burns SWTCH to fund 365 epochs (1 year) for each developer
- Developer calls
- Developers get free access without needing to hold SWTCH
Scenario C: Corporate Treasury Management
- Company has 20 employees with subscriptions
- Company treasury (multisig) holds all SWTCH tokens
- Each month, treasury calls
subscription_extendfor all 20 employees - Centralizes billing and token management
- Employees don't need individual SWTCH balances
Scenario A: Testing New Features
- Program admin needs to test premium features
- Admin calls
subscription_extendwith:token_amount = 100(100 epochs)admin_extend = true
- No tokens are burnt
- Test subscription is extended by 100 epochs
- Event shows
admin_extend = true,tokens_burnt = 0
Scenario B: Customer Support
- User reports subscription issue and needs immediate access
- Support team member (in admin_list) extends subscription
- Calls
subscription_extendwithadmin_extend = true - User gets 30 days of free access while issue is investigated
- No tokens required from either party
Scenario C: Promotional Campaign
- Switchboard runs "Free Month" promotion for new users
- Admin extends all qualifying subscriptions by 30 epochs
- Batch calls
subscription_extendwithadmin_extend = true - Thousands of users get free access
- No token burning required
Scenario D: Non-Admin Attempts Admin Extend
- Regular user tries to call
subscription_extendwithadmin_extend = true - Validation fails: payer is not in state.authority or state.admin_list
- Returns error:
NotAuthorizedAsAdmin - User must pay normally or request admin assistance
- Pricing calculation edge cases
- Token time multiplier calculations
- Epoch calculation (Solana vs custom)
- Admin, override, and token list management
- Subscription extension with active/expired subscriptions
- Multiple payment token scenarios
- Tier change pro-rating:
- Upgrade during active subscription (should reduce epochs)
- Downgrade during active subscription (should increase epochs)
- Change settings when expired (should not affect epochs)
- Edge case: Upgrade causes subscription to expire (rounds to 0)
- Multi-user access:
- Add/remove authorized users
- Cannot exceed 16 authorized users
- Cannot add owner to authorized users
- Cannot add duplicate users
- Authorized users can access but not manage subscription
- Permissionless extension:
- Anyone can extend any subscription
- Non-owner payer correctly tracked in events
- Gifting and sponsorship scenarios
- Admin extend:
- Admin can extend with admin_extend=true flag
- Non-admin cannot use admin_extend=true (returns error)
- No tokens burnt when admin_extend=true
- Event correctly shows admin_extend flag and zero tokens burnt
- Full subscription lifecycle with different tokens
- Multiple users sharing same subscription
- Admin configuration changes
- Token burning and vault management for multiple tokens
- Token addition, update, and disabling
- Mixed payment scenarios (user pays with different tokens over time)
- Team/Enterprise scenarios:
- Owner adds 16 team members
- Team members can access API
- Team members cannot modify subscription
- Owner removes team members
- Shared rate limits across all users
- Gifting/Sponsorship scenarios:
- Third party extends another user's subscription
- DAO sponsors multiple developer subscriptions
- Corporate treasury pays for employee subscriptions
- Event tracking shows correct payer vs owner
- Admin extension scenarios:
- Admin extends subscription without burning tokens
- Promotional campaigns with batch admin extensions
- Customer support emergency access
- Security: non-admin cannot use admin_extend flag
- Integer overflow protection in pricing calculations
- Token multiplier bounds checking
- Authority verification for all admin operations
- PDA derivation validation
- Token account ownership verification
- Payment token validation (must be accepted and enabled)
- Burn vault matching for each token
- Multi-user access control:
- Only owner can manage authorized users
- Authorized users cannot escalate privileges
- Prevent owner from being added to authorized users list
- Validate user authorization on every API request
- Oracle service must check both owner and authorized_users array
- Admin extension security:
- Verify payer is admin when admin_extend=true flag is set
- Prevent privilege escalation through admin_extend flag
- Track admin extensions separately in events for audit trail
- Admin list should be carefully managed and minimal
- Subscription NFTs: Issue NFTs representing subscription status
- Referral System: Discount codes and referral rewards
- Per-User Roles: Different permission levels (read-only, full-access, admin)
- Dynamic Pricing: Market-based pricing adjustments
- Grace Period: Allow limited access for X days after expiration
- Subscription Transfers: Allow transferring ownership to another wallet
- Auto-renewal: Opt-in automatic extension when balance available
- Usage Analytics: Track per-user API usage for billing insights
- User Invitations: Allow users to accept invitation before being added
programs/subscriber/
├── Cargo.toml
└── src/
├── lib.rs # Program entry point
├── error.rs # Error definitions
├── event.rs # Event definitions
├── utils.rs # Pricing calculations with token multipliers
├── impls/
│ ├── mod.rs
│ ├── subscriber_state_impl.rs # SubscriberState account with token config
│ └── subscription_impl.rs # Subscription account
└── actions/
├── mod.rs
├── state/
│ ├── mod.rs
│ ├── state_init.rs
│ ├── state_set_config.rs
│ ├── state_manage_admins.rs
│ ├── state_manage_overrides.rs
│ └── state_manage_tokens.rs # NEW: Token management
└── subscription/
├── mod.rs
├── subscription_init.rs # Just creates account
├── subscription_set_params.rs # Configure tier settings
├── subscription_manage_users.rs # NEW: Add/remove authorized users
├── subscription_extend.rs # Updated: Multi-token support
└── subscription_close.rs
This design provides a flexible, scalable subscription system for Switchboard oracle data access. The program:
- Supports multiple subscription tiers with configurable pricing based on a baseline cost (
base_price_per_epoch) - Accepts multiple payment tokens (SWTCH, USDC, or any configured SPL token)
- Uses configurable time multipliers to adjust subscription duration per token
- Provides token burning mechanism for payment
- Provides admin controls for pricing configuration and token management
- Allows override accounts for permanent free access
- Tracks subscription periods using Solana or custom epochs
- Properly handles subscription extensions and expirations with different tokens
- Supports granular rate limiting with separate limits for oracle and crossbar requests
- Uses separate cost curves for oracle and crossbar request pricing
- Emits detailed events for off-chain tracking including token usage
- Follows existing Switchboard program patterns and conventions
- Multi-token Support: Admins can configure up to 16 different payment tokens, each with its own burn vault and time multiplier
- Flexible Pricing: Token time multipliers allow for economic adjustments (e.g., 2x multiplier = double subscription time)
- Granular Tiers: Users configure delay in milliseconds, oracle/crossbar request limits, feed limits, and asset streams independently
- Linear Delay Pricing Curve: Instead of discrete pricing tiers, uses a smooth linear curve from max to min multiplier based on exact delay_ms value
- Eliminates pricing discontinuities
- Provides fine-grained cost optimization opportunities
- Formula:
multiplier = max - (delay_ms * slope), clamped to[min, max] - Example: With proper slope, 3000ms delay costs ~54% less than 0ms, while 5000ms costs ~90% less
- Separate Rate Limiting: Oracle (on-chain) and Crossbar (off-chain) requests have independent limits and cost curves
- Configurable Baseline Costs: Admins can set a baseline cost per epoch that serves as the foundation for all pricing calculations
- Administrative Control: Separate admin lists for configuration changes and override management
- Historical Tracking: Subscriptions track last payment token and amounts for analytics
- Multi-User Subscriptions: Each subscription supports up to 16 authorized users who can share access to the subscription benefits (owner + 16 = 17 total users per subscription)
- Permissionless Extensions: Anyone can pay to extend any subscription, enabling gifting, sponsorship, and flexible corporate billing models
- Admin Extensions: Program admins can extend subscriptions without burning tokens using the admin_extend flag, useful for testing, support, and promotional campaigns