External Plugins
Oracle Plugin
Last updated January 31, 2026
The Oracle Plugin connects Core Assets to external Oracle accounts for custom validation logic. Reject transfers, burns, or updates based on time, price, ownership, or any custom rule you implement.
What You'll Learn
- Create Oracle accounts for validation
- Configure lifecycle checks (reject transfers, updates, burns)
- Use PDA-based Oracle addressing
- Deploy time-based and condition-based restrictions
Summary
The Oracle Plugin validates lifecycle events against external Oracle accounts. The Oracle account stores validation results that the Core program reads to approve or reject operations.
- Can reject create, transfer, burn, and update events
- Oracle account is external to the Asset (you control it)
- Dynamic: update the Oracle account to change behavior
- Supports PDA derivation for per-asset or per-owner oracles
Out of Scope
AppData storage (see AppData Plugin), built-in freeze behavior (see Freeze Delegate), and off-chain oracles.
Quick Start
Jump to: Oracle Account Structure · Create with Oracle · Add to Asset · Default Oracles
- Deploy an Oracle account with validation rules
- Add Oracle plugin adapter to Asset/Collection
- Configure lifecycle checks (which events to validate)
- Update Oracle account to change validation behavior dynamically
What is an Oracle Plugin?
An Oracle Plugin is an onchain account that is created by the authority externally from the MPL Core Asset or Collection. If an Asset or Collection has an Oracle Adapter enabled and an Oracle Account assigned to it the Oracle Account will be loaded by the MPL Core program for validations against lifecycle events. The Oracle Plugin stores data relating to 4 lifecycle events of create, transfer, burn, and update and can be configured to perform a Reject validation. The ability to update and change the Oracle Account provides a powerful and interactive lifecycle experience.
Works With
| MPL Core Asset | ✅ |
| MPL Core Collection | ✅ |
Allowed Validations
The following validation results can be returned from the Oracle Account to the Oracle Plugin.
| Can Approve | ❌ |
| Can Reject | ✅ |
| Can Pass | ❌ |
On Chain Oracle Account Structure
The Oracle Account should have the following onchain account structure.
On Chain Account Struct of Oracle Account
#[account]
pub struct Validation {
pub validation: OracleValidation,
}
impl Validation {
pub fn size() -> usize {
8 // anchor discriminator
+ 5 // validation
}
}
pub enum OracleValidation {
Uninitialized,
V1 {
create: ExternalValidationResult,
transfer: ExternalValidationResult,
burn: ExternalValidationResult,
update: ExternalValidationResult,
},
}
pub enum ExternalValidationResult {
Approved,
Rejected,
Pass,
}
Oracle Account Offset
The account structure will differ slightly between account frameworks (Anchor, Shank, etc.) due to the discriminator sizes needed for accounts:
- If the
OracleValidationstruct is located at the beginning of the data section for the Oracle account, then chooseNoOffsetfor theValidationResultsOffset. - If the Oracle account only contains the
OracleValidationstruct but is managed by an Anchor program, selectAnchorforValidationResultsOffsetso that the struct can be located after the Anchor account discriminator. - If the
OracleValidationstruct is located at some other offset in the Oracle account, use theCustomoffset.
resultsOffset / result_offset
const resultsOffset: ValidationResultsOffset =
| { type: 'NoOffset' }
| { type: 'Anchor' }
| { type: 'Custom'; offset: bigint };
Updating the Oracle Account
Because the Oracle Account is created and maintained by the creator/developer the OracleValidation struct can be updated at anytime allowing lifecycles to be dynamic.
The Oracle Adapter
The Oracle Adapter accepts the following arguments and data.
On Chain Struct
pub struct Oracle {
/// The address of the oracle, or if using the `pda` option,
/// a program ID from which to derive a PDA.
pub base_address: Pubkey,
/// Optional account specification (PDA derived from `base_address` or other
/// available account specifications). Note that even when this
/// configuration is used there is still only one
/// Oracle account specified by the adapter.
pub base_address_config: Option<ExtraAccount>,
/// Validation results offset in the Oracle account.
/// Default is `ValidationResultsOffset::NoOffset`
pub results_offset: ValidationResultsOffset,
}
Declaring the PDA of an Oracle Plugin
The default behavior of the Oracle Plugin Adapter is to supply the adapter with a static base_address which the adapter can then read from and provide the resulting validation results. If you wish to get more dynamic with the Oracle Plugin Adapter you can pass in your program_id as the base_address and then an ExtraAccount, which can be used to derive one or more PDAs pointing to Oracle Account addresses. This allows the Oracle Adapter to access data from multiple derived Oracle Accounts. Note that there are other advanced non-PDA specifications also available when using ExtraAccount.
List of ExtraAccounts Options
An example of an extra account that is the same for all assets in a collection is the PreconfiguredCollection PDA, which uses the collection's Pubkey to derive the Oracle account. An example of more dynamic extra account is the PreconfiguredOwner PDA, which uses the owner pubkey to derive the Oracle account.
pub enum ExtraAccount {
/// Program-based PDA with seeds \["mpl-core"\]
PreconfiguredProgram {
/// Account is a signer
is_signer: bool,
/// Account is writable.
is_writable: bool,
},
/// Collection-based PDA with seeds \["mpl-core", collection_pubkey\]
PreconfiguredCollection {
/// Account is a signer
is_signer: bool,
/// Account is writable.
is_writable: bool,
},
/// Owner-based PDA with seeds \["mpl-core", owner_pubkey\]
PreconfiguredOwner {
/// Account is a signer
is_signer: bool,
/// Account is writable.
is_writable: bool,
},
/// Recipient-based PDA with seeds \["mpl-core", recipient_pubkey\]
/// If the lifecycle event has no recipient the derivation will fail.
PreconfiguredRecipient {
/// Account is a signer
is_signer: bool,
/// Account is writable.
is_writable: bool,
},
/// Asset-based PDA with seeds \["mpl-core", asset_pubkey\]
PreconfiguredAsset {
/// Account is a signer
is_signer: bool,
/// Account is writable.
is_writable: bool,
},
/// PDA based on user-specified seeds.
CustomPda {
/// Seeds used to derive the PDA.
seeds: Vec<Seed>,
/// Program ID if not the base address/program ID for the external plugin.
custom_program_id: Option<Pubkey>,
/// Account is a signer
is_signer: bool,
/// Account is writable.
is_writable: bool,
},
/// Directly-specified address.
Address {
/// Address.
address: Pubkey,
/// Account is a signer
is_signer: bool,
/// Account is writable.
is_writable: bool,
},
}
Creating and Adding Oracle Plugins
Creating an Asset with the Oracle Plugin
Create a MPL Core Asset with an Oracle Plugin
import { generateSigner, publicKey } from '@metaplex-foundation/umi'
import {
create,
CheckResult
} from '@metaplex-foundation/mpl-core'
const collectionSigner = generateSigner(umi)
const oracleAccount = publicKey('11111111111111111111111111111111')
const asset = await create(umi, {
... CreateAssetArgs,
plugins: [
{
type: 'Oracle',
resultsOffset: {
type: 'Anchor',
},
baseAddress: oracleAccount,
lifecycleChecks: {
update: [CheckResult.CAN_REJECT],
},
baseAddressConfig: undefined,
},
],
});.sendAndConfirm(umi)
Adding an Oracle Plugin to An Asset
Adding an Oracle Plugin to a Collection
import { publicKey } from '@metaplex-foundation/umi'
import { addPlugin, CheckResult } from '@metaplex-foundation/mpl-core'
const asset = publicKey('11111111111111111111111111111111')
const oracleAccount = publicKey('22222222222222222222222222222222')
addPlugin(umi, {
asset,
plugin: {
type: 'Oracle',
resultsOffset: {
type: 'Anchor',
},
lifecycleChecks: {
create: [CheckResult.CAN_REJECT],
},
baseAddress: oracleAccount,
},
})
Creating a Collection with an Oracle Plugin
Creating a Collection with an Oracle Plugin
import { generateSigner, publicKey } from '@metaplex-foundation/umi'
import {
create,
CheckResult
} from '@metaplex-foundation/mpl-core'
const collectionSigner = generateSigner(umi)
const oracleAccount = publicKey('11111111111111111111111111111111')
const collection = await createCollection(umi, {
... CreateCollectionArgs,
plugins: [
{
type: 'Oracle',
resultsOffset: {
type: 'Anchor',
},
baseAddress: oracleAccount,
lifecycleChecks: {
update: [CheckResult.CAN_REJECT],
},
baseAddressConfig: undefined,
},
],
});.sendAndConfirm(umi)
Adding an Oracle Plugin to a Collection
Adding an Oracle Plugin to a Collection
import { publicKey } from '@metaplex-foundation/umi'
import { addCollectionPlugin, CheckResult } from '@metaplex-foundation/mpl-core'
const collection = publicKey('11111111111111111111111111111111')
const oracleAccount = publicKey('22222222222222222222222222222222')
await addCollectionPlugin(umi, {
collection: collection,
plugin: {
type: 'Oracle',
resultsOffset: {
type: 'Anchor',
},
lifecycleChecks: {
update: [CheckResult.CAN_REJECT],
},
baseAddress: oracleAccount,
},
}).sendAndConfirm(umi)
Default Oracles deployed by Metaplex
In some rare cases like Soulbound NFT it might be useful to have Oracles that always Deny or Approve a lifecycle event. For those the following Oracles have been deployed and can be used by anyone:
- Transfer Oracle: Always denies transferring.
AwPRxL5f6GDVajyE1bBcfSWdQT58nWMoS36A1uFtpCZY - Update Oracle: Always denies updating.
6cKyMV4toCVCEtvh6Sh5RQ1fevynvBDByaQP4ufz1Zj6 - Create Oracle: Always denies creating.
2GhRFi9RhqmtEFWCwrAHC61Lti3jEKuCKPcZTfuujaGr
Example Usage/Ideas
Example 1
Assets to be not transferable during the hours of noon-midnight UTC.
- Create onchain Oracle Plugin in a program of your choice.
- Add the Oracle Plugin Adapter to an Asset or Collection specifying the lifecycle events you wish to have rejection validation over.
- You write a cron that writes and updates to your Oracle Plugin at noon and midnight flipping a bit validation from true/false/true.
Example 2
Assets can only be updated if the floor price is above $10 and the asset has attribute “red hat”.
- Create onchain Oracle Plugin in a program of your choice.
- Add the Oracle Plugin Adapter to Asset specifying the lifecycle events you wish to have rejection validation over.
- Dev writes Anchor program that can write to the Oracle Account that derive the same PRECONFIGURED_ASSET accounts
- Dev writes web2 script that watches prices on a marketplace, AND with known hashlist of Assets with the 'Red Hat' trait red updates and writes to the relevant Oracle Accounts.
Common Errors
Oracle validation failed
The Oracle account returned a rejection. Check your Oracle account state and validation logic.
Oracle account not found
The Oracle account address is invalid or doesn't exist. Verify the base address and any PDA derivation.
Invalid results offset
The ValidationResultsOffset doesn't match your Oracle account structure. Use Anchor for Anchor programs, NoOffset for raw accounts.
Notes
- Oracle accounts are external. You deploy and maintain them
- Validation is read-only: Core reads the Oracle, doesn't write to it
- Use cron jobs or event listeners to update Oracle state dynamically
- PDA derivation allows per-asset, per-owner, or per-collection oracles
FAQ
Can Oracle plugins approve transfers, or only reject?
Oracle plugins can only reject or pass. They cannot force-approve transfers that other plugins reject.
How do I create time-based transfer restrictions?
Deploy an Oracle account and use a cron job to update the validation result at specific times. See Example 1 above.
Can I use one Oracle for multiple Assets?
Yes. Use a static base address for a single Oracle, or use PDA derivation with PreconfiguredCollection for collection-wide validation.
What's the difference between Oracle and Freeze Delegate?
Freeze Delegate is built-in and binary (frozen/unfrozen). Oracle allows custom logic—time-based, price-based, or any condition you implement.
Do I need to write a Solana program for Oracle?
Yes. The Oracle account must be a Solana account with the correct structure. You can use Anchor or native Rust. See the Oracle Plugin Example guide.
Glossary
| Term | Definition |
|---|---|
| Oracle Account | External Solana account storing validation results |
| Oracle Adapter | Plugin component attached to Asset referencing the Oracle |
| ValidationResultsOffset | Byte offset to locate validation data in Oracle account |
| ExtraAccount | PDA derivation configuration for dynamic Oracle addressing |
| Lifecycle Check | Configuration specifying which events the Oracle validates |
Related Pages
- External Plugins Overview - Understanding external plugins
- AppData Plugin - Data storage instead of validation
- Oracle Plugin Example - Complete implementation guide
- Freeze Delegate - Built-in freeze alternative
