Features
Execute Asset Signing
Last updated September 1, 2026
The MPL Core Execute instruction introduces the concept of Asset Signers to MPL Core Assets. These Asset Signers act as Signers on behalf of the Asset itself which unlocks the ability for MPL Core Assets
- to transfer out Solana and SPL Tokens.
- to become the authority of other accounts.
- to perform other actions and validations that have been assigned to the
assetSignerPdathat require transaction/instruction/CPI signing. MPL Core Assets have the ability to sign and submit transactions/CPIs to the blockchain. This effectively gives the Core Asset it's own wallet in the form of anassetSigner.
Withdraw Asset Signer balances before burning
Burning a Core Asset makes the execute instruction fail. The program can no longer load the Asset, so it cannot sign as the assetSignerPda. Any SOL, tokens, or other assets still held by that PDA become stranded with no recovery path.
Asset Signer PDA
Assets are now able to access the assetSignerPda account/address which allows the execute instruction on the MPL Core program to pass through additional instructions sent to it to sign the CPI instructions with the assetSignerPda. This allows the assetSignerPda account to effectively own and execute account instructions on behalf of the current asset owner. You can think of the assetSignerPda as a wallet attached to a Core Asset.
Fund the Asset Signer PDA, not the Asset address
The assetSignerPda is a different address from the Asset account. Only the assetSignerPda can hold SOL and tokens on behalf of the Asset. The Asset account itself must never be used as a wallet.
findAssetSignerPda()
const assetId = publickey('11111111111111111111111111111111')
const assetSignerPda = findAssetSignerPda(umi, { asset: assetId })
Execute Instruction
Overview
The execute instruction allows users to pass in the Core Asset and also some pass through instructions that will get signed by the AssetSigner when it hits the MPL Core programs execute instruction on chain. An overview of the execute instruction and it's args.
const executeIx = await execute(umi, {
{
// The asset via `fetchAsset()` that is signing the transaction.
asset: AssetV1,
// The collection via `fetchCollection()`
collection?: CollectionV1,
// Either a TransactionBuilder | Instruction[]
instructions: ExecuteInput,
// Additional Signers that will be required for the transaction/instructions.
signers?: Signer[]
}
})
Validation
assetSignerPda Validation
The MPL Core Execute instruction will validate that the current Asset owner has also signed the transaction. This insures only the current Asset Owner can execute transactions while using the assetSignerPda with the execute instruction.
Controlling Execute Operations
The execute functionality can be controlled using the Freeze Execute Plugin. This plugin allows you to freeze the execute operations on an asset, preventing any execute instructions from being processed until unfrozen. The Freeze Execute Plugin is particularly useful for:
- Backed NFTs: Prevent withdrawal of underlying assets when needed
- Escrowless protocols: Temporarily lock execute functionality during protocol operations
- Security measures: Add an additional layer of protection for assets that can execute complex operations When the Freeze Execute Plugin is active and set to
frozen: true, any attempts to use the execute instruction will be blocked until the plugin is updated tofrozen: false.
Burning an Asset with Asset Signer balances
Burning a Core Asset permanently disables execute, so any SOL, tokens, or nested Core Assets left in the assetSignerPda cannot be moved.
The execute instruction requires a live Core Asset account owned by the MPL Core program. After you burn the Asset, that account is no longer a valid Asset. The assetSignerPda address still exists and can still hold funds — there is just no remaining instruction that can spend them.
Move everything out of the PDA with execute before burning:
- Derive the PDA with
findAssetSignerPdaormplx core asset execute info - Transfer SOL, SPL tokens, and any Core Assets the PDA owns
- Confirm the PDA is empty
- Burn the Asset
Examples
Transferring SOL From the Asset Signer
In the following example we transfer SOL that had been sent to the assetSignerPda to a destination of our choice.
import {
execute,
findAssetSignerPda,
fetchAsset,
fetchCollection,
} from '@metaplex-foundation/mpl-core'
import { transferSol } from '@metaplex-foundation/mpl-toolbox'
import { publickey, createNoopSigner, sol } from '@metaplex-foundation/umi'
const assetId = publickey('11111111111111111111111111111111')
const asset = await fetchAsset(umi, assetId)
// Optional - If Asset is part of collection fetch the collection object
const collection =
asset.updateAuthority.type == 'Collection' && asset.updateAuthority.address
? await fetchCollection(umi, asset.updateAuthority.address)
: undefined
// Asset signer has a balance of 1 SOL in the account.
const assetSignerPda = findAssetSignerPda(umi, { asset: assetId })
// Destination account we wish to transfer the SOL to.
const destination = publickey('2222222222222222222222222222222222')
// A standard `transferSol()` transactionBuilder.
const transferSolIx = transferSol(umi, {
// Create a noopSigner as the assetSigner will sign later during CPI
source: createNoopSigner(publicKey(assetSigner)),
// Destination address
destination,
// Amount you wish to transfer
amount: sol(0.5),
})
// Call the `execute` instruction and send to the chain.
const res = await execute(umi, {
// Execute instruction(s) with this asset
asset,
// If Asset is part of collection pass in collection object via `fetchCollection()`
collection,
// The transactionBuilder/instruction[] to execute
instructions: transferSolIx,
}).sendAndConfirm(umi)
console.log({ res })
Transferring SPL Tokens From the Asset Signer
In the following example we transfer some of our SPL Token balance from the assetSignerPda account to a destination. This example is based on the best practices in regards to derived tokens accounts for a base wallet address. If tokens are not in their correctly derived token account based on the assetSignerPda address then this example will need adjusting.
import {
execute,
findAssetSignerPda,
fetchAsset,
fetchCollection,
} from '@metaplex-foundation/mpl-core'
import {
transferTokens,
findAssociatedTokenPda,
} from '@metaplex-foundation/mpl-toolbox'
import { publickey } from '@metaplex-foundation/umi'
const assetId = publickey('11111111111111111111111111111111')
const asset = await fetchAsset(umi, assetId)
// Optional - If Asset is part of collection fetch the collection object
const collection =
asset.updateAuthority.type == 'Collection' && asset.updateAuthority.address
? await fetchCollection(umi, asset.updateAuthority.address)
: undefined
const splTokenMint = publickey('2222222222222222222222222222222222')
// Asset signer has a balance of tokens.
const assetSignerPda = findAssetSignerPda(umi, { asset: assetId })
// Destination wallet we wish to transfer the SOL to.
const destinationWallet = publickey('3333333333333333333333333333333')
// A standard `transferTokens()` transactionBuilder.
const transferTokensIx = transferTokens(umi, {
// Source is the `assetSignerPda` derived Token Account
source: findAssociatedTokenPda(umi, {
mint: splTokenMint,
owner: assetSignerPda,
}),
// Destination is the `destinationWallet` derived Token Account
destination: findAssociatedTokenPda(umi, {
mint: splTokenMint,
owner: destinationWallet,
}),
// Amount to send in lamports.
amount: 5000,
})
// Call the `execute` instruction and send to the chain.
const res = await execute(umi, {
// Execute instruction(s) with this asset
asset,
// If Asset is part of collection pass in collection object via `fetchCollection()`
collection,
// The transactionBuilder/instruction[] to execute
instructions: transferTokensIx,
}).sendAndConfirm(umi)
console.log({ res })
Transferring Ownership of an Asset to Another Asset
In the following example we transfer a Core Asset that is owned by another Core Asset, to another.
import {
execute,
fetchAsset,
fetchCollection,
findAssetSignerPda,
transfer,
} from '@metaplex-foundation/mpl-core'
import { publickey } from '@metaplex-foundation/umi'
// Asset we wish to transfer.
const assetId = publickey('11111111111111111111111111111111')
const asset = await fetchAsset(assetId)
// Optional - If Asset is part of collection fetch the collection object
const collection =
asset.updateAuthority.type == 'Collection' && asset.updateAuthority.address
? await fetchCollection(umi, asset.updateAuthority.address)
: undefined
// Asset ID that owns the Asset we wish to transfer.
const sourceAssetId = publickey('2222222222222222222222222222222222')
// The source Asset object.
const sourceAsset = fetchAsset(umi, sourceAssetId)
// Asset signer has a balance of 1 SOL in the account.
const sourceAssetSignerPda = findAssetSignerPda(umi, { asset: assetId })
// Destination account we wish to transfer the SOL to.
const destinationAssetId = publickey('33333333333333333333333333333333')
// Destination Asset signer we wish to transfer the Asset to.
const destinationAssetSignerPda = findAssetSignerPda(umi, {
asset: destinationAssetId,
})
const transferAssetIx = transfer(umi, {
// Asset object via `fetchAsset()`.
asset,
// Optional - Collection object via `fetchCollection()`
collection,
// New Owner of the Asset.
newOwner: destinationAssetSignerPda,
}).sendAndConfirm(umi)
const res = await execute(umi, {
// Execute instruction(s) with this asset
asset,
// If Asset is part of collection pass in collection object via `fetchCollection()`
collection,
// The transactionBuilder/instruction[] to execute
instructions: transferAssetIx,
}).sendAndConfirm(umi)
console.log({ res })
Notes
- Only the current Asset owner can invoke
execute(the owner must sign the outer transaction) - The Freeze Execute Plugin can block
executeuntil it is unfrozen - Burning the Asset is irreversible for Asset Signer funds: empty the PDA first
- The
assetSignerPdais deterministic from the Asset address and does not change if the Asset is transferred - The
executeinstruction charges a protocol fee paid by thepayeraccount passed to the instruction, which is normally the Asset owner but can also be theassetSignerPdaitself. This is separate from the Solana transaction fee, which the PDA cannot pay. See the Protocol Fees page for the current amount. The fee is transferred into the Asset account and later swept by the Metaplex fee collector. - Every lamport above the rent-exempt minimum on the Asset account is treated as a protocol fee and will be collected. Hold SOL and tokens in the
assetSignerPda, never in the Asset account. - The
assetSignerPdais a system-owned account. The Solana runtime rejects any transaction that leaves it with a non-zero balance below the rent-exempt minimum for a zero-byte account, so a transfer out must either empty the account or leave at least that minimum behind. If the PDA is also thepayerforexecute, the protocol fee is deducted from it in the same instruction, so include the fee when working out what remains. QuerygetMinimumBalanceForRentExemption(0)on the target cluster for the current minimum instead of hard-coding a value.
FAQ
What happens to funds in the Asset Signer PDA if I burn the Asset?
They are stranded. execute requires a live Core Asset, so after burn the PDA can still hold SOL, tokens, and nested Assets but nothing can sign to move them.
Can I recover stranded Asset Signer funds after a burn?
No. There is no instruction that can sign as the assetSignerPda without the original Asset account.
Does transferring the Asset strand the Asset Signer wallet?
No. Transfer changes which owner can call execute. The PDA stays attached to the Asset address. Burning, not transferring, is what disables execute.
