Skip to main content
Every reserve needs a valid oracle. Oracle configuration lives inside the reserve config under tokenInfo. Reserves are priced through Scope, Kamino’s price aggregator.

How pricing works

Scope combines provider feeds (Chainlink and Pyth Pro) into a single, manipulation-resistant chain of indices, and exposes custom oracle types for fixed-income, pegged, multi-source, and LST assets. You wire a reserve to a Scope feed by setting scopeConfiguration in tokenInfo: the price chain, TWAP chain, and guards. For an asset that isn’t in Scope yet, coordinate with the Kamino team to add it before listing.

Required tokenInfo fields

FieldTypeWhat it does
namestring (≤32 bytes UTF-8)Asset symbol
maxAgePriceSecondsu64Reject the spot price if older than this many seconds
maxAgeTwapSecondsu64Reject the TWAP if older than this many seconds. Required if TWAP is enabled
maxTwapDivergenceBpsu64Maximum divergence between spot and TWAP in basis points. 0 disables the guard
heuristic{ lower, upper, exp }Sanity bounds. Reject prices outside [lower * 10^exp, upper * 10^exp]. All zeros disables
blockPriceUsage0 or 1If 1, the reserve is configured but the program rejects any operation needing a price (emergency switch)
Plus the Scope feed:
  • scopeConfiguration — Scope feed pubkey + price chain + TWAP chain

Update oracle config via SDK

The most direct path for changing just the Scope oracle on an existing reserve is kaminoManager.updateReserveScopeOracleConfigurationIxs. For broader reserve updates that include oracle changes, use updateReserveIxs with a full ReserveConfig.
1

Initialize KaminoManager and fetch the reserve

import {
  createSolanaRpc,
  createSolanaRpcSubscriptions,
  address,
  pipe,
  createTransactionMessage,
  setTransactionMessageFeePayerSigner,
  setTransactionMessageLifetimeUsingBlockhash,
  appendTransactionMessageInstructions,
  signTransactionMessageWithSigners,
  sendAndConfirmTransactionFactory,
} from '@solana/kit';
import {
  KaminoManager,
  DEFAULT_RECENT_SLOT_DURATION_MS,
  PROGRAM_ID,
  MarketWithAddress,
} from '@kamino-finance/klend-sdk';
import { LendingMarket, Reserve } from '@kamino-finance/klend-sdk/dist/@codegen/klend/accounts';
import { parseKeypairFile } from '@kamino-finance/klend-sdk/dist/utils/signer.js';

const adminSigner = await parseKeypairFile('/path/to/admin.json');
const rpc = createSolanaRpc('https://api.mainnet-beta.solana.com');
const rpcSubscriptions = createSolanaRpcSubscriptions('wss://api.mainnet-beta.solana.com');

const kaminoManager = new KaminoManager(rpc, DEFAULT_RECENT_SLOT_DURATION_MS, PROGRAM_ID);

const marketAddress = address('<MARKET_ADDRESS>');
const reserveAddress = address('<RESERVE_ADDRESS>');

const market = await LendingMarket.fetch(rpc, marketAddress);
if (!market) throw new Error('Market not found');
const marketWithAddress: MarketWithAddress = { address: marketAddress, state: market };
2

Update Scope oracle configuration

const updateIxs = await kaminoManager.updateReserveScopeOracleConfigurationIxs(
  adminSigner,
  marketWithAddress,
  reserveAddress,
  {
    scopePriceConfigAddress: address('3NJYftD5sjVfxSnUdZ1wVML8f3aC6mp1CXCL6L7TnU8C'),
    scopeChain:     [0, 65535, 65535, 65535],
    scopeTwapChain: [52, 65535, 65535, 65535],
  },
);

async function buildAndSendTx(ixs: any[]) {
  const { value: blockhash } = await rpc.getLatestBlockhash({ commitment: 'finalized' }).send();
  const signed = await signTransactionMessageWithSigners(
    pipe(
      createTransactionMessage({ version: 0 }),
      (tx) => setTransactionMessageFeePayerSigner(adminSigner, tx),
      (tx) => setTransactionMessageLifetimeUsingBlockhash(blockhash, tx),
      (tx) => appendTransactionMessageInstructions(ixs, tx)
    )
  );
  await sendAndConfirmTransactionFactory({ rpc, rpcSubscriptions })(signed, {
    commitment: 'confirmed',
  });
}

for (const { ixs } of updateIxs) {
  await buildAndSendTx(ixs);
}
65535 (u16::MAX) is the unused-slot sentinel. A single-hop Scope chain uses [N, 65535, 65535, 65535].
3

Adjusting the TWAP / staleness guards

To change the staleness and TWAP guards, fetch the reserve’s full config, mutate the relevant tokenInfo fields, and apply via updateReserveIxs:
const reserve = await Reserve.fetch(rpc, reserveAddress);
if (!reserve) throw new Error('Reserve not found');

const newConfig = { ...reserve.config };
newConfig.tokenInfo = {
  ...reserve.config.tokenInfo,
  maxAgePriceSeconds: 120n,
  maxAgeTwapSeconds: 240n,
  maxTwapDivergenceBps: 4050n,
};

const updateIxs = await kaminoManager.updateReserveIxs(
  adminSigner,
  marketWithAddress,
  reserveAddress,
  newConfig,
);

for (const { ixs } of updateIxs) {
  await buildAndSendTx(ixs);
}

Looking up oracle IDs

import { ScopeConfig } from '@kamino-finance/scope-sdk';

const scopeConfig = await ScopeConfig.fetch(rpc, address('<SCOPE_CONFIG_PUBKEY>'));
// Inspect scopeConfig.tokensMetadata to find the index for your asset
Or call the kamino-manager CLI’s get-oracle-mappings command (next tab) to retrieve the canonical IDs.

TWAP and staleness guards

The two guards work together to reject manipulated or stale prices.
GuardWhat it catches
maxAgePriceSecondsSpot price older than this is rejected. Set to 120 for liquid assets, lower for high-stakes reserves
maxAgeTwapSecondsTWAP older than this is rejected. Typically 2–4× maxAgePriceSeconds
maxTwapDivergenceBpsIf spot deviates from TWAP by more than this, the price is rejected. Set to 4050 (≈40.5%) for volatile assets, lower for stables. 0 disables
If TWAP is enabled (maxTwapDivergenceBps > 0) the Scope feed must expose a TWAP chain. The on-chain validator returns InvalidTwapConfig if you enable the guard against a feed with no TWAP.

Price heuristic

The heuristic is an absolute price band:
"heuristic": {
  "lower": 9,
  "upper": 11,
  "exp": 1
}
With exp: 1, the price is accepted only when 0.9 ≤ price ≤ 1.1. Useful for stables you expect to peg. Set all zeros to disable.

Block price usage (emergency)

blockPriceUsage: 1 is an emergency switch: the reserve is configured normally but the program refuses any operation that needs the price. Use it when you need to halt a reserve immediately while preparing a real config update.

Choosing for new assets

Asset typeRecommended setup
Major stablecoins (USDC, USDT, USDS)Scope, with a tight heuristic and TWAP guard
Major liquid assets (SOL, BTC, ETH)Scope
LSTs (mSOL, jitoSOL, INF, etc.)Scope (uses LST → SOL → USD chain)
Pegged or fixed-income assetsScope custom oracle types (CappedFloored, DiscountToMaturity)
Tokenized off-chain assetsCoordinate with the Kamino team to add to Scope

Reference