Account Layout Rust struct
Each price entry is stored in a PDA derived from the token mint and the feeder's public key. Multiple feeders produce independent entries for the same token.
| Field | Type | Description |
|---|
| mint | Pubkey | Token mint address |
| price_usd | u64 | Price × 10⁹ (9 decimals) |
| timestamp | u64 | Unix time of last update |
| feeder | Pubkey | Feeder that wrote this entry |
| token_name | String | Human-readable name |
seeds = ["price_feed", mint, feeder]
Read price in a dApp JavaScript
Derive the PDA for a known mint + feeder pair, fetch the account, and decode the 9-decimal fixed-point price. No special SDK required — plain @solana/web3.js is enough.
// Derive the PDA
const [pda] = PublicKey.findProgramAddressSync(
[
new TextEncoder().encode("price_feed"),
mint.toBytes(),
feeder.toBytes(),
],
new PublicKey("PROGRAM_ID")
);
// Fetch and decode
const info = await connection.getAccountInfo(pda);
const d = new DataView(info.data.buffer);
const price = d.getBigUint64(40, true); // offset 40
const ts = d.getBigUint64(48, true); // offset 48
// Convert to float: divide by 1_000_000_000
const priceUsd = Number(price) / 1e9;Get all feeders for a token JavaScript
Use getProgramAccounts filtered by the 8-byte PriceFeed discriminator to retrieve every entry on-chain, then filter client-side by mint.
// Anchor discriminator = SHA-256("account:PriceFeed")[0..8]
const disc = new Uint8Array(
(await crypto.subtle.digest(
"SHA-256",
new TextEncoder().encode("account:PriceFeed")
))
).slice(0, 8);
const accounts = await connection
.getProgramAccounts(PROGRAM_ID, {
filters: [{ dataSize: 125 }]
});
// Pick the freshest price across all feeders
const best = accounts
.map(({ account }) => decode(account.data))
.filter(f => f.mint === targetMint.toBase58())
.sort((a, b) => Number(b.timestamp - a.timestamp))[0];List approved tokens JavaScript
Fetch all ApprovedToken PDAs to discover which mints are tracked by the oracle. Each PDA is seeded from ["approved_token", mint] and is exactly 117 bytes.
// ApprovedToken::SPACE = 117 bytes
const accounts = await connection
.getProgramAccounts(PROGRAM_ID, {
filters: [{ dataSize: 117 }]
});
accounts.forEach(({ account }) => {
const d = new Uint8Array(account.data);
const view = new DataView(d.buffer);
let off = 8; // skip discriminator
const mint = new PublicKey(d.slice(off, off+32)).toBase58(); off += 32;
const nameLen = view.getUint32(off, true); off += 4;
const name = new TextDecoder().decode(d.slice(off, off+nameLen));
console.log(`${name}: ${mint}`);
});seeds = ["approved_token", mint]
Invoke get_price JavaScript
Call the get_price instruction to have the program validate freshness, feeder registration, and token approval on-chain. It emits a PriceQueried event with the verified price data. Useful when you need a guaranteed on-chain validation rather than a client-side check.
const disc = createHash("sha256")
.update("global:get_price").digest().slice(0, 8);
const [priceFeed] = PublicKey.findProgramAddressSync(
[Buffer.from("price_feed"), mint.toBytes(), feeder.toBytes()],
PROGRAM_ID
);
const [registry] = PublicKey.findProgramAddressSync(
[Buffer.from("registry")], PROGRAM_ID
);
const [approvedToken] = PublicKey.findProgramAddressSync(
[Buffer.from("approved_token"), mint.toBytes()], PROGRAM_ID
);
const ix = new TransactionInstruction({
programId: PROGRAM_ID,
keys: [
{ pubkey: priceFeed, isSigner: false, isWritable: false },
{ pubkey: registry, isSigner: false, isWritable: false },
{ pubkey: approvedToken, isSigner: false, isWritable: false },
],
data: Buffer.from(disc),
});
// Simulate (read-only — no SOL spent)
const result = await connection.simulateTransaction(tx);
// Parse PriceQueried event from logs
// Event disc = sha256("event:PriceQueried")[0..8]
// Layout: mint(32), feeder(32), price_usd(u64), timestamp(u64),
// token_name(4+N), age_secs(u64)Get median price JavaScript
Call get_median_price to compute the statistical median across all active feeders. The program filters out stale feeds (> 90 s) and feeds from deregistered feeders, then emits a MedianPriceQueried event.
// 1. Fetch all PriceFeed PDAs for the mint
const allFeeds = await connection.getProgramAccounts(PROGRAM_ID, {
filters: [{ dataSize: 125 }],
});
const remaining = allFeeds
.filter(({ account }) =>
new PublicKey(account.data.slice(8, 40)).toBase58() === mint.toBase58()
)
.map(({ pubkey }) =>
({ pubkey, isSigner: false, isWritable: false })
);
// 2. Build instruction — registry + approvedToken + remaining feeds
const disc = createHash("sha256")
.update("global:get_median_price").digest().slice(0, 8);
const ix = new TransactionInstruction({
programId: PROGRAM_ID,
keys: [
{ pubkey: registry, isSigner: false, isWritable: false },
{ pubkey: approvedToken, isSigner: false, isWritable: false },
...remaining,
],
data: Buffer.from(disc),
});
// 3. Simulate, then parse MedianPriceQueried event from logs
// Event disc = sha256("event:MedianPriceQueried")[0..8]
// Layout: mint(32), token_name(4+N), median_price_usd(u64),
// feeder_count(u8), timestamp(u64)CPI from another program Anchor / Rust
Pass the PriceFeed PDA as an account to your program and deserialize it directly. No CPI call needed — just borrow the account data and check staleness.
// In your Anchor program:
#[account(
seeds = [
b"price_feed",
mint.key().as_ref(),
feeder.key().as_ref(),
],
bump,
seeds::program = ORACLE_PROGRAM_ID
)]
pub price_feed: Account<'info, PriceFeed>,
// Then in your instruction handler:
let feed = &ctx.accounts.price_feed;
let age = clock.unix_timestamp as u64
- feed.timestamp;
require!(age <= 90, ErrorCode::StalePrice);
let price = feed.price_usd; // u64, 9 decimalsFeederRecord account Reference
Each registered feeder has a FeederRecord PDA (seeds: ["feeder", feeder_pubkey], size: 41 bytes). Its existence on-chain proves the feeder is authorized. The update_price instruction validates this PDA rather than scanning the registry Vec. To check if a feeder is authorized, test whether the account exists:
const [feederRecord] = PublicKey.findProgramAddressSync(
[Buffer.from("feeder"), feederPubkey.toBytes()],
PROGRAM_ID
);
const authorized = (await connection.getAccountInfo(feederRecord)) !== null;| Offset | Size | Field | Decode as |
|---|
| 0 | 8 B | Discriminator | Skip (Anchor header) |
| 8 | 32 B | feeder | new PublicKey(data.slice(8, 40)) |
| 40 | 1 B | bump | data[40] |