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 assetSignerPda that 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 an assetSigner.

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 to frozen: 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:

  1. Derive the PDA with findAssetSignerPda or mplx core asset execute info
  2. Transfer SOL, SPL tokens, and any Core Assets the PDA owns
  3. Confirm the PDA is empty
  4. 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 execute until it is unfrozen
  • Burning the Asset is irreversible for Asset Signer funds: empty the PDA first
  • The assetSignerPda is deterministic from the Asset address and does not change if the Asset is transferred
  • The execute instruction charges a protocol fee paid by the payer account passed to the instruction, which is normally the Asset owner but can also be the assetSignerPda itself. 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 assetSignerPda is 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 the payer for execute, the protocol fee is deducted from it in the same instruction, so include the fee when working out what remains. Query getMinimumBalanceForRentExemption(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.