On-Chain Pricing Overview
A dToken (e.g., dBLUE) represents a programmable, self-custodial basket of underlying assets on Solana. To calculate the real-time net asset value (NAV) and price of a dToken, integrator platforms need to:
Identify if a single token is a DiversiFi dToken (via PDA derivation).
Or, retrieve a list of all active dTokens directly from the network.
Parse the active basket constituents (mints).
Derive the underlying Vault Token Accounts (ATAs).
Fetch balance snapshots for all underlying vault token accounts.
Calculate NAV and price using the balances, live constituent prices, and the dToken's total supply.
1. How to Determine if a Token is a dToken (Deriving the Index PDA)
Currently, all DiversiFi indexes are managed by the DiversiFi smart contract. You can verify if a token is a dToken by checking if an Index Account PDA exists for that token mint under the DiversiFi Program ID.
DiversiFi Program ID: 3vyr9DRfMZb2KvUQdnps7YG3PY38XdguLBQaJ2DFkSxk
Before checking if the token is valid, you must derive its corresponding Index Address. The Index Account is a Program Derived Address (PDA) deterministically calculated using the constant string "index" and the dToken's mint address.
Seeds:
Buffer.from("index")dTokenMint.toBuffer()
const [indexPda] = PublicKey.findProgramAddressSync(
[Buffer.from("index"), dTokenMint.toBuffer()],
new PublicKey("3vyr9DRfMZb2KvUQdnps7YG3PY38XdguLBQaJ2DFkSxk")
);Once derived, checking validity is a matter of verifying that the account exists. If the Connection.getAccountInfo(indexPda) JSON-RPC call returns non-null data, the token is a valid DiversiFi dToken.
2. Getting a List of All Active dTokens on the Network
If you don't have a specific token to check, and instead want to pull a fresh list of all currently deployed dTokens from on-chain, you can query the Solana RPC.
Because the token_mint address (the dToken mint) is consistently located at byte offset 33 of the Index struct layout, you can use the dataSlice RPC parameter to fetch only the 32-byte dToken mint addresses from every Index account on the network. This makes the query extremely fast and avoids downloading heavy account data when it's not needed.
3. Parsing the Account/Data Structure for the Basket
The Index Account uses a 1-byte custom discriminator ([1]) rather than the standard Anchor 8-byte discriminator. This is why the dataSlice offset in Section 2 is 33 (1 + 32), not 40. The Account struct layout is:
Since the mints array is fixed at 5, you must filter out any empty slots by checking if the mint address equals PublicKey.default (11111111111111111111111111111111).
Fetching using raw Borsh
Since the protocol IDL is subject to change, the recommended approach is to decode the account directly using @coral-xyz/borsh:
4. Getting the List of Tokens in the Basket and Deriving PDAs
The underlying tokens composing the basket are stored in Associated Token Accounts (ATAs) owned by the Index PDA. Note that some underlying tokens might be standard Token program tokens, while others might use the Token2022 program.
To get the vault addresses, derive the ATA for each valid mint from the activeMints array, with the indexPda acting as the wallet owner.
5. Fetching Balances using On-Chain Simulation
While you can fetch multiple ATAs separately using getMultipleAccountsInfo, DiversiFi provides an atomic on-chain instruction (get_vault_balances) that returns all vault balances synchronously in one lightweight call. This avoids inconsistencies caused by RPC slot lag.
Note: This instruction is read-only and is intended to be executed via simulateTransaction.
The get_vault_balances Instruction
get_vault_balances InstructionDiscriminator: [138, 38, 110, 31, 116, 102, 223, 172]
Accounts Required:
index(The Index PDA - read-only)token_mint(The dToken Mint - read-only)remainingAccounts(The list of vault ATAs derived in Step 4 - read-only)
Borsh Return Schema:
By simulating the transaction, the program will return the decoded state atomically via the transaction return data.
See price_derivation.ts, function fetchVaultBalancesWithInstruction(), for a complete working implementation.
6. Calculating the NAV and Price
Once you have the vault token balances, their decimals, and the dToken index supply, calculating the NAV requires fetching live oracle prices (e.g., from Pyth or Switchboard) for each constituent.
Step A. Total USD Value of the Basket (NAV)
Step B. Real-Time dToken Price
7. Example TypeScript Implementation
See the provided price_derivation.ts script in this repository for a complete end-to-end working example using the @solana/web3.js and @coral-xyz/anchor libraries.
8. Reading NAV from a Switchboard On-Chain Oracle Account
For baskets that have a published Switchboard On-Demand NAV feed, the simplest read path is to fetch a single canonical on-chain account and decode 16 bytes. No constituent math, no off-chain price source, no SDK dependency required for the read path.
Use this section if you want a single up-to-date NAV value for a dToken. If you need the underlying constituent breakdown, see Sections 1–7. If you need atomic freshness inside your own swap / quote transaction, see §8.4.
8.1 Mechanics
The on-chain account is a "canonical OracleQuote" — its address is deterministic from (queue_pubkey, feed_id). There is no registry to query; once you know the feed_id for a basket, you can derive the account address client-side and read from it.
Account data layout (the slice you care about):
The value is fixed-point with 18 decimal places. Divide by 1e18 to get USD per dToken LP token.
8.2 Constants
Switchboard On-Demand mainnet program
exposed as ON_DEMAND_MAINNET_PID from @switchboard-xyz/on-demand
Mainnet queue
A43DyUGA7s8eXPxqEjJY6EBu1KKbNgfxF8h17VAHn13w
Switchboard quote program
orac1eFjzWL5R3RbbdMV68K9H6TaCVVcL6LjvQQWAbz
Live NAV feeds (canonical quote account = OracleQuote.getCanonicalPubkey(queue, [feedId])):
Basket
feedId (V2)
Canonical quote account
dBONK
0x9a5cfb9568ca6c9eeb9833ea0fbfb2a9e163f50d78fad56411010d386ea0c19f
FjzDEtknQEfiN4cgynvXFeukH9DNEHTc7YFFchtRP1vP
dYIELD
0x9f83c3e1f4f26a0c2646cd79cb9be1246b04552153b7170dd3e8ed4330d1d7d4
J3UmBcrkysdptEYVBWuC6WGwcycafpTTzb9N6q3iHJMz
dBIG4 (stored as dMULTI in DB)
0x4484de63de1cc245b30467d5f3b28781eea4df1557d6a18909c55117e3a17969
5QPMhXYAVm4bHY9NsdyU2Zi4gbbJN3UrVSzAD6tEKq3K
dBOOMER
0xde956aa58cfb0b01d5b11a8f0b555a1b3437b281
For baskets not listed above, contact DiversiFi for the current feedId; a public registry endpoint is on the roadmap. Note: the user-facing dBIG4 basket is stored under the symbol dMULTI in the DiversiFi DB, but the on-chain feed and quote account use the same canonical layout as every other basket — the symbol divergence is purely a metadata concern.
8.3 Read pattern (TypeScript, no Switchboard SDK required)
The same pattern works in Rust, Python, or any client that can fetch a Solana account — only the BigInt arithmetic and PDA derivation API differ by language. The Switchboard SDK exposes the equivalent of findProgramAddressSync(...) as OracleQuote.getCanonicalPubkey(queue, [feedId]) if you prefer to add the dependency.
8.4 Atomic-crank pattern (for trade execution)
The default-read pattern (§8.3) returns whatever value the account holds at read time. DiversiFi runs a low-cadence keeper that refreshes the account roughly every 15 minutes, so passive reads see a value that is at most ~15 minutes stale.
If your use case can't tolerate that staleness — e.g. you're quoting a swap or routing through a dToken in your own AMM — include a "crank" in the same transaction that consumes the value. The on-chain quote account will be updated atomically with whatever else your tx does:
After this transaction confirms, OracleQuote holds the value the oracle just signed, and your instruction consumed exactly that value.
Cost & footprint per atomic crank:
Tx fee added
10,000 lamports
Standard 5,000-lamport base × 2 (signature + Ed25519 precompile)
Compute units added
~628
Ed25519 runs as a native precompile, doesn't count
Tx-bytes added
~330
Ed25519 ix data + quote-program ix data
Latency added
~2–5 s
Switchboard gateway round-trip during fetchManagedUpdateIxs
The CU footprint is negligible. Tx-bytes can matter if you're already near the 1232-byte legacy tx limit — use a v0 transaction with lookup tables in that case.
8.5 Freshness guarantees
Passive read (§8.3)
≤ 15 minutes stale (DiversiFi keeper cadence; subject to change — contact us if you need a tighter SLA)
Atomic crank (§8.4)
Zero staleness — value is signed by the oracle within the same tx
8.6 When to use which
Dashboards, indexers, analytics
§8.3 — passive read, no SDK dep, ≤15 min freshness
AMM / orderbook / routing / arbitrage where stale NAV is exploitable
§8.4 — atomic crank in your tx
You need per-constituent token breakdowns or to avoid trusting a published NAV
Sections 1–7 — manual computation from on-chain state
8.7 Error handling
accountInfo === null— the canonical account has not yet been bootstrapped on-chain. The basket may not have a feed yet, or the first crank hasn't fired. Contact DiversiFi.accountInfo.data.indexOf(feedHashBytes) === -1— thefeedIdyou used does not match what's stored in this account. Double-check thefeedId.fetchManagedUpdateIxsthrows — the Switchboard gateway is transiently unavailable, the feed is mis-configured, or the oracle subnet can't execute the underlying job. Retry once; if it persists, fall back to §8.3 or contact DiversiFi.
8.8 Example TypeScript Implementation
See oracle_nav_read.ts in this repository for a runnable example covering both the passive read (§8.3) and the atomic-crank pattern (§8.4):
The passive read works with the base dependencies of this repo. The atomic-crank demo additionally requires the Switchboard SDK packages — install them only if you need that path:
Last updated
