Skip to main content

Overview

Kamino Lending’s referral system allows referrers to earn a portion of protocol fees generated by users they refer. Referrers create a unique state account and URL, then receive fees when referred users borrow, repay, or use flash loans.

Referrer State Structures

The referral system uses three main account types defined in state/referral.rs:

ReferrerState

// state/referral.rs:119
pub struct ReferrerState {
    pub short_url: Pubkey,  // Link to ShortUrl account
    pub owner: Pubkey,       // Referrer's wallet
}
The main referrer account linking the referrer to their short URL.

ShortUrl

// state/referral.rs:126
pub struct ShortUrl {
    pub referrer: Pubkey,    // Referrer's wallet
    pub short_url: String,   // Custom URL identifier
}
Stores the referrer’s custom short URL for user tracking.

ReferrerTokenState

// state/referral.rs:38
pub struct ReferrerTokenState {
    pub referrer: Pubkey,              // Referrer's wallet
    pub mint: Pubkey,                  // Token mint (one per asset)
    pub amount_unclaimed_sf: u128,     // Unclaimed fees (scaled fraction)
    pub amount_cumulative_sf: u128,    // Total fees earned (scaled fraction)
    pub bump: u64,                     // PDA bump seed
    pub padding: [u64; 31],
}
Tracks fees earned per token. Referrers have one ReferrerTokenState per reserve/mint. Display Format (state/referral.rs:64):
let amount_unclaimed = Fraction::from_bits(*amount_unclaimed_sf)
    .saturating_to_num::<u64>();
let amount_cumulative = Fraction::from_bits(*amount_cumulative_sf)
    .saturating_to_num::<u64>();
Fees are stored as scaled fractions for precision, converted to integers for display.

Setting Up as a Referrer

Step 1: Initialize Referrer State and Short URL

Handler: handler_init_referrer_state_and_short_url.rs:11
pub fn init_referrer_state_and_short_url(
    ctx: Context<InitReferrerStateAndShortUrl>,
    short_url: String,
) -> Result<()>
This instruction creates your ReferrerState and ShortUrl accounts. Requirements (handler_init_referrer_state_and_short_url.rs:12):
require!(
    short_url
        .chars()
        .all(|char| char.is_ascii_alphanumeric() || char == '_' || char == '-'),
    LendingError::ShortUrlNotAsciiAlphanumeric
);
  • Short URL must contain only ASCII alphanumeric characters, underscores, or hyphens
  • Must have a UserMetadata account already created
Accounts:
  • referrer: Signer and fee payer
  • referrer_state: PDA ["referrer_state", referrer.key()]
  • referrer_short_url: PDA ["short_url", short_url.bytes()]
  • referrer_user_metadata: Your existing user metadata account
Example:
import { PublicKey } from '@solana/web3.js';
import { initReferrerStateAndShortUrl } from './kamino-sdk';

const setupReferrer = async (shortUrl: string) => {
  // Find PDAs
  const [referrerState] = PublicKey.findProgramAddressSync(
    [Buffer.from("referrer_state"), wallet.publicKey.toBuffer()],
    KAMINO_PROGRAM_ID
  );
  
  const [shortUrlAccount] = PublicKey.findProgramAddressSync(
    [Buffer.from("short_url"), Buffer.from(shortUrl)],
    KAMINO_PROGRAM_ID
  );
  
  const [userMetadata] = PublicKey.findProgramAddressSync(
    [Buffer.from("user_metadata"), wallet.publicKey.toBuffer()],
    KAMINO_PROGRAM_ID
  );
  
  // Create instruction
  const ix = await initReferrerStateAndShortUrl({
    referrer: wallet.publicKey,
    referrerState,
    referrerShortUrl: shortUrlAccount,
    referrerUserMetadata: userMetadata,
    shortUrl,
  });
  
  const tx = new Transaction().add(ix);
  await sendAndConfirmTransaction(connection, tx, [wallet]);
  
  console.log(`Referrer setup complete! Short URL: ${shortUrl}`);
};

// Usage
await setupReferrer("my_referral_link");

Step 2: Initialize Referrer Token State

Handler: handler_init_referrer_token_state.rs:8
pub fn init_referrer_token_state(
    ctx: Context<InitReferrerTokenState>,
) -> Result<()>
Create a ReferrerTokenState for each reserve/token you want to earn fees from. Initialization (handler_init_referrer_token_state.rs:14):
*referrer_token_state = ReferrerTokenState {
    referrer,
    mint: reserve.liquidity.mint_pubkey,
    amount_unclaimed_sf: 0,
    amount_cumulative_sf: 0,
    bump: bump.into(),
    padding: [0; 31],
};
Accounts:
  • payer: Fee payer (can be different from referrer)
  • lending_market: The lending market
  • reserve: The reserve to track fees for
  • referrer: The referrer’s wallet
  • referrer_token_state: PDA ["referrer_token_state", referrer.key(), reserve.key()]
Example:
const initTokenState = async (reserve: PublicKey, referrer: PublicKey) => {
  const [referrerTokenState, bump] = PublicKey.findProgramAddressSync(
    [
      Buffer.from("referrer_token_state"),
      referrer.toBuffer(),
      reserve.toBuffer(),
    ],
    KAMINO_PROGRAM_ID
  );
  
  const ix = await initReferrerTokenState({
    payer: wallet.publicKey,
    lendingMarket,
    reserve,
    referrer,
    referrerTokenState,
  });
  
  await sendAndConfirmTransaction(connection, new Transaction().add(ix), [wallet]);
};

// Initialize for USDC reserve
await initTokenState(usdcReserve, wallet.publicKey);

How Referrers Earn Fees

Referrers earn fees when their referred users perform these actions:

1. Borrowing

Fee Accumulation (lending_operations.rs:324):
if let Some(mut referrer_token_state) = referrer_token_state {
    if lending_market.referral_fee_bps > 0 {
        add_referrer_fee(
            borrow_reserve,
            &mut referrer_token_state,
            Fraction::from_num(referrer_fee),
        )?;
        borrow_reserve.liquidity.total_available_amount += referrer_fee;
    }
}
When a user borrows with a referrer, the referrer earns a portion of the origination fee.

2. Flash Loans

Fee Accumulation (lending_operations.rs:1791):
if let Some(referrer_token_state_loader) = referrer_token_state_loader {
    if lending_market.referral_fee_bps > 0 {
        let referrer_token_state = &mut referrer_token_state_loader.get_mut()?;
        add_referrer_fee(
            reserve,
            referrer_token_state,
            Fraction::from_num(referrer_fee),
        )?;
        reserve.liquidity.total_available_amount += referrer_fee;
    }
}
Referrers earn fees when referred users execute flash loans.

3. Ongoing Interest

Continuous Accrual (lending_operations.rs:1959):
pub fn accumulate_referrer_fees<'info, T>(
    program_id: &Pubkey,
    borrow_reserve_info_key: Pubkey,
    borrow_reserve: &mut Reserve,
    obligation_referrer: &Pubkey,
    lending_market_referral_fee_bps: u16,
    slots_elapsed: u64,
    borrowed_amount_f: Fraction,
    previous_borrowed_amount_f: Fraction,
    obligation_has_referrer: bool,
    referrer_token_states_iter: &mut impl Iterator<Item = T>,
) -> Result<()>
Referrers continuously earn fees from the interest accrued on their referred users’ borrows. Fee Calculation (lending_operations.rs:2001):
let referrer_fee_f = net_new_variable_debt_f * absolute_referral_rate;

Fee Addition Logic

Implementation (lending_operations.rs:1944):
pub fn add_referrer_fee(
    borrow_reserve: &mut Reserve,
    referrer_token_state: &mut ReferrerTokenState,
    referrer_fee: Fraction,
) -> Result<()> {
    let referrer_fee_sf = referrer_fee.to_sf();
    referrer_token_state.amount_unclaimed_sf += referrer_fee_sf;
    referrer_token_state.amount_cumulative_sf += referrer_fee_sf;
    borrow_reserve.liquidity.accumulated_referrer_fees_sf += referrer_fee_sf;
    Ok(())
}
Fees are tracked in both the ReferrerTokenState and the reserve’s accumulated fees.

Claiming Referrer Fees

withdraw_referrer_fees Instruction

Handler: handler_withdraw_referrer_fees.rs:16
pub fn withdraw_referrer_fees(
    ctx: Context<WithdrawReferrerFees>,
) -> Result<()>
Withdraws accumulated referrer fees to your token account. Withdrawal Logic (lending_operations.rs:2040):
pub fn withdraw_referrer_fees(
    reserve: &mut Reserve,
    slot: Slot,
    referrer_token_state: &mut ReferrerTokenState,
) -> Result<u64> {
    // Reserve must be fresh
    if reserve.last_update.is_stale(slot, PriceStatusFlags::ALL_CHECKS)? {
        return err!(LendingError::ReserveStale);
    }
    
    let withdraw_amount = reserve.get_withdraw_referrer_fees(referrer_token_state);
    
    if withdraw_amount == 0 {
        return err!(LendingError::InsufficientReferralFeesToRedeem);
    }
    
    reserve.withdraw_referrer_fees(withdraw_amount, referrer_token_state)?;
    reserve.last_update.mark_stale();
    
    Ok(withdraw_amount)
}
Accounts:
  • referrer: Signer (must be the referrer owner)
  • referrer_token_state: PDA ["referrer_token_state", referrer.key(), reserve.key()]
  • reserve: The reserve to withdraw fees from
  • reserve_liquidity_mint: The reserve’s token mint
  • reserve_supply_liquidity: The reserve’s supply vault
  • referrer_token_account: Your token account (receives fees)
  • lending_market: The lending market
  • lending_market_authority: PDA signer
Example:
const claimReferrerFees = async (reserve: PublicKey) => {
  // Load accounts
  const reserveData = await loadReserve(reserve);
  const [referrerTokenState] = PublicKey.findProgramAddressSync(
    [
      Buffer.from("referrer_token_state"),
      wallet.publicKey.toBuffer(),
      reserve.toBuffer(),
    ],
    KAMINO_PROGRAM_ID
  );
  
  // Get or create token account
  const referrerTokenAccount = await getOrCreateAssociatedTokenAccount(
    connection,
    wallet,
    reserveData.liquidity.mintPubkey,
    wallet.publicKey
  );
  
  // Withdraw fees
  const ix = await withdrawReferrerFees({
    referrer: wallet.publicKey,
    referrerTokenState,
    reserve,
    reserveLiquidityMint: reserveData.liquidity.mintPubkey,
    reserveSupplyLiquidity: reserveData.liquidity.supplyVault,
    referrerTokenAccount: referrerTokenAccount.address,
    lendingMarket,
    lendingMarketAuthority,
  });
  
  const tx = new Transaction().add(ix);
  const sig = await sendAndConfirmTransaction(connection, tx, [wallet]);
  
  console.log(`Fees claimed! Signature: ${sig}`);
};

Complete Setup and Claiming Example

import { Connection, PublicKey, Transaction } from '@solana/web3.js';
import { KaminoLending } from './kamino-sdk';

class ReferralManager {
  constructor(
    private connection: Connection,
    private wallet: Wallet,
    private programId: PublicKey
  ) {}
  
  // Step 1: One-time setup
  async setupReferrer(shortUrl: string) {
    // Create referrer state and short URL
    const setupIx = await this.createSetupInstruction(shortUrl);
    await this.sendTransaction([setupIx]);
    
    console.log(`✓ Referrer state created with URL: ${shortUrl}`);
  }
  
  // Step 2: Initialize token states for reserves you want to track
  async initializeTokenStates(reserves: PublicKey[]) {
    for (const reserve of reserves) {
      const ix = await this.createInitTokenStateInstruction(reserve);
      await this.sendTransaction([ix]);
      console.log(`✓ Token state initialized for reserve: ${reserve}`);
    }
  }
  
  // Step 3: Monitor unclaimed fees
  async getUnclaimedFees(reserve: PublicKey): Promise<number> {
    const [referrerTokenState] = PublicKey.findProgramAddressSync(
      [
        Buffer.from("referrer_token_state"),
        this.wallet.publicKey.toBuffer(),
        reserve.toBuffer(),
      ],
      this.programId
    );
    
    const accountInfo = await this.connection.getAccountInfo(referrerTokenState);
    if (!accountInfo) return 0;
    
    // Deserialize ReferrerTokenState
    const data = accountInfo.data;
    const amountUnclaimedSf = data.readBigUInt64LE(64); // Offset to amount_unclaimed_sf
    
    // Convert scaled fraction to decimal
    const unclaimed = Number(amountUnclaimedSf) / (2 ** 60); // Approximate
    return unclaimed;
  }
  
  // Step 4: Claim all accumulated fees
  async claimAllFees(reserves: PublicKey[]) {
    for (const reserve of reserves) {
      const unclaimed = await this.getUnclaimedFees(reserve);
      
      if (unclaimed > 0) {
        const ix = await this.createWithdrawFeesInstruction(reserve);
        await this.sendTransaction([ix]);
        console.log(`✓ Claimed ${unclaimed} fees from reserve: ${reserve}`);
      }
    }
  }
  
  private async sendTransaction(instructions: TransactionInstruction[]) {
    const tx = new Transaction().add(...instructions);
    return await sendAndConfirmTransaction(this.connection, tx, [this.wallet]);
  }
}

// Usage
const manager = new ReferralManager(connection, wallet, KAMINO_PROGRAM_ID);

// One-time setup
await manager.setupReferrer("my_kamino_link");
await manager.initializeTokenStates([usdcReserve, solReserve, ethReserve]);

// Regular fee claims
setInterval(async () => {
  await manager.claimAllFees([usdcReserve, solReserve, ethReserve]);
}, 24 * 60 * 60 * 1000); // Daily

Fee Distribution

Referral fees are a percentage of protocol fees, controlled by lending_market.referral_fee_bps:
// If referral_fee_bps = 2000 (20%)
// And user pays 100 USDC in borrow fees:
const protocolFee = 100;
const referralFeeBps = 2000; // 20%
const referrerFee = protocolFee * (referralFeeBps / 10000);
// referrerFee = 20 USDC to referrer
// 80 USDC to protocol

Best Practices

  1. Initialize token states early: Set up ReferrerTokenState for all reserves you expect users to interact with
  2. Regular claims: Claim fees regularly to compound earnings
  3. Monitor metrics: Track cumulative vs unclaimed fees to measure referral performance
  4. User experience: Share your short URL prominently for easy user attribution
  5. Token account management: Ensure you have token accounts for all mints before claiming

Troubleshooting

InsufficientReferralFeesToRedeem:
  • No unclaimed fees available
  • Wait for referred users to generate more activity
ShortUrlNotAsciiAlphanumeric:
  • Use only letters, numbers, underscores, and hyphens in short URLs
ReserveStale:
  • Reserve needs to be refreshed before withdrawing fees
  • Ensure you call refresh_reserve in the same transaction or beforehand

Build docs developers (and LLMs) love