Oracles
How Project 0 prices assets with Pyth, Switchboard, Scope and exchange-rate oracles, and how confidence intervals, EMA and staleness apply.
Each Bank is configured with an oracle setup by the Group administrator. The setup names the price source stored in bank.config.oracle_keys[0] and, for some setups, one or two extra on-chain accounts the program reads alongside it to derive the final price.
Oracle setups
| Setup | Price source (oracle_keys[0]) | Extra accounts | How the price is formed |
|---|---|---|---|
Pyth — PythPushOracle, and the KaminoPythPush / DriftPythPull / SolendPythPull / JuplendPythPull venue variants | Pyth price update account | Venue reserve / market in oracle_keys[1] for the venue variants | Pyth price and confidence, kept fresh by Pyth's own infrastructure; no caller action. |
Switchboard — SwitchboardPull and its venue variants | Switchboard pull feed | Venue account in oracle_keys[1] for the venue variants | Feed value; the caller must crank the feed just before use. |
Scope — Scope | A Scope OraclePrices account; bank.config.scope_entry_index selects one of its 512 entries | None | The entry's value / 10^exp. Scope carries no confidence interval; risk is expressed through the bank's weights. Program 0.1.11. |
mSOL — PythMSOL, KaminoMSOL, JuplendMSOL | Pyth SOL/USD | Marinade State (oracle_keys[1]; oracle_keys[2] on the venue variants, whose [1] is the venue account) | SOL/USD × the mSOL/SOL rate derived from Marinade's virtual staked balance over the mSOL supply. Program 0.1.11. |
LST — PythLST, KaminoLST, JuplendLST | Pyth SOL/USD | SPL or Sanctum stake pool (same slots as mSOL) | SOL/USD × total_lamports / pool_token_supply. The pool must have been updated in the current or previous epoch. Program 0.1.11. |
Exponent PT — PTPyth, PTFixed | Pyth base feed for PTPyth; none for PTFixed (the underlying is treated as $1) | Exponent vault (oracle_keys[1] for PTPyth, oracle_keys[0] for PTFixed) | Base × a linear rate that accretes from the bank's configured start price to par at the vault's maturity, capped by what the PT can actually redeem for. Program 0.1.11. |
Fixed — Fixed and its venue variants | None | Venue account for the venue variants | An administrator-set constant (rare, special cases). |
For the mSOL, LST and PT setups the exchange rate is part of the oracle price itself — an mSOL bank's price is mSOL/USD. The receipt-token rate of a venue (a Kamino cToken, a JupLend fToken) is a separate step applied on top by the risk engine, exactly as it is for every other venue bank.
Oracle Confidence Interval Adjustment
Some oracles report a price with a confidence interval (P +/- c), representing the range within which the true price likely falls. P0 uses confidence intervals conservatively:
- Assets (collateral) are priced at the lower bound:
P - P * c - Liabilities (debt) are priced at the upper bound:
P + P * c
Example: If the oracle reports Token A at $20 with a 5% confidence interval:
- As collateral, Token A is valued at $20 - $1 = $19
- As debt, Token A is valued at $20 + $1 = $21
The confidence interval is capped at a maximum of 5%. If the oracle reports a wider confidence interval, the protocol may clamp it to 5% or abort the transaction entirely. This is relatively unique to P0 in the borrow-lending space and provides an additional layer of protection against oracle instability. Scope prices and fixed prices have no confidence interval, so their collateral and liability values coincide.
EMA vs Spot Price
Some oracles report both a spot price (the current instantaneous price) and an Exponential Moving Average (EMA) price (a price-over-time instead of an instant price "right now").
When both are available, P0 uses them strategically:
| Operation | Price Used |
|---|---|
| Borrowing | EMA price (more stable, harder to manipulate) |
| Liquidation | Spot price (more responsive to rapid changes) |
If an EMA price is not available, all operations use the spot price.
Staleness
Oracle data must be fresh: every bank has an oracle_max_age (seconds), and a transaction that depends on an older price fails.
- Pyth oracles are rarely stale since Pyth maintains its own update cadence.
- Switchboard oracles require the caller to send a crank instruction just before the transaction to refresh the data.
- Scope entries are refreshed by the feed operator; there is nothing for a caller to crank. A bank's
oracle_max_ageis applied to the entry's own timestamp with no default — an age of0is always stale. - mSOL, LST and PT setups inherit the freshness of their Pyth base feed. A stake pool additionally counts as stale if it has not been updated in the current or previous epoch.
When composing transactions that use Switchboard oracles, either bundle the crank instruction in the same transaction (if compute and account limits allow) or send a separate crank transaction with a brief delay before the main transaction. Many operators use Jito bundles for this.
Oracle Configuration
Each Bank's oracle configuration is readable from bank.config.oracle_setup, bank.config.oracle_keys and, for Scope banks, bank.config.scope_entry_index.
When constructing a transaction that requires a risk engine check, every active bank contributes its accounts to the remaining accounts in this order — the bank, then its oracle accounts exactly as the program expects them:
| Setup | Accounts per bank |
|---|---|
| Pyth, Switchboard, Scope, Fixed (plain) | bank, oracle_keys[0] |
| Any venue variant (Kamino, Drift, Solend, JupLend) | bank, oracle_keys[0], oracle_keys[1] (venue account) |
PythMSOL, PythLST, PTPyth | bank, oracle_keys[0], oracle_keys[1] (Marinade State / stake pool / vault) |
KaminoMSOL, JuplendMSOL, KaminoLST, JuplendLST | bank, oracle_keys[0], oracle_keys[1] (venue account), oracle_keys[2] (Marinade State / stake pool) |
PTFixed | bank, oracle_keys[0] (the vault) |
Native stake (StakedWithPythPush) | bank, oracle_keys[0], oracle_keys[1], oracle_keys[2], and the pool on-ramp |
The TypeScript SDK builds this list for you (computeHealthAccountMetas); see Program Upgrade 0.1.11 if you assemble it yourself. For Kamino specifically, refresh_reserve must also execute within the same slot.