solana-metaplex-nft — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited solana-metaplex-nft (Agent Skill) and scored it 100/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 0 flagged
Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
Metaplex is the dominant NFT standard on Solana, powering nearly every NFT minted on the network. As of 2026, two standards coexist: the classic Token Metadata program (NFTs built on SPL tokens — what powered DeGods, Mad Lads, etc.) and MPL-Core, the newer single-account standard that is cheaper to mint, simpler to compose with plugins, and the default recommendation for new collections. This skill defaults to MPL-Core and treats Token Metadata as legacy. All flows here use the Umi client framework (the supported successor to the deprecated JS SDK).
Install the pinned versions (verified May 2026):
npm install \
@metaplex-foundation/umi@^1.5.1 \
@metaplex-foundation/umi-bundle-defaults@^1.5.1 \
@metaplex-foundation/mpl-core@^1.10.0 \
@metaplex-foundation/mpl-token-metadata@^3.4.0 \
@metaplex-foundation/umi-uploader-irys@^1.4.1
npm install -D tsx typescript @types/nodeA devnet wallet keypair at ~/.config/solana/id.json works out of the box. Fund it with solana airdrop 2 --url devnet before running anything that uploads to Irys.
Umi is a lightweight framework you compose with plugins. Order matters: install signers before uploaders so the uploader can sign funding transactions.
import { createUmi } from '@metaplex-foundation/umi-bundle-defaults'
import { mplCore } from '@metaplex-foundation/mpl-core'
import { keypairIdentity } from '@metaplex-foundation/umi'
import { irysUploader } from '@metaplex-foundation/umi-uploader-irys'
const umi = createUmi('https://api.devnet.solana.com')
.use(mplCore())
.use(keypairIdentity(yourSigner))
.use(irysUploader({ address: 'https://devnet.irys.xyz' }))For mainnet, drop the address option — Irys auto-selects the mainnet bundler (https://uploader.irys.xyz / https://node1.irys.xyz) and pays in SOL.
Irys (formerly Bundlr) is the standard for paying in SOL to write to permanent Arweave storage. Use the helpers:
import { createGenericFile } from '@metaplex-foundation/umi'
import fs from 'node:fs'
const imageBytes = fs.readFileSync('./assets/nft.png')
const file = createGenericFile(imageBytes, 'nft.png', {
tags: [{ name: 'Content-Type', value: 'image/png' }],
})
const [imageUri] = await umi.uploader.upload([file])
const metadataUri = await umi.uploader.uploadJson({
name: 'My NFT',
description: '...',
image: imageUri,
// ...
})This is what marketplaces (Magic Eden, Tensor, etc.) parse to render your NFT:
{
"name": "Asset name (≤ 32 chars on-chain)",
"symbol": "SHORT",
"description": "Long-form description shown on marketplaces.",
"image": "https://arweave.net/<image-tx-id>",
"external_url": "https://yourproject.xyz",
"attributes": [
{ "trait_type": "Background", "value": "Blue" },
{ "trait_type": "Rarity", "value": "Legendary" }
],
"properties": {
"files": [
{ "uri": "https://arweave.net/<image-tx-id>", "type": "image/png" }
],
"category": "image",
"creators": [
{ "address": "<wallet>", "share": 100 }
]
}
}The on-chain name and uri are stored directly; everything else lives in this JSON. On-chain name is capped at 32 bytes — keep the long version inside the JSON.
MPL-Core (recommended) — single account, ~0.0029 SOL rent:
import { createCollection, mplCore } from '@metaplex-foundation/mpl-core'
import { generateSigner } from '@metaplex-foundation/umi'
const collection = generateSigner(umi)
await createCollection(umi, {
collection,
name: 'My Collection',
uri: collectionMetadataUri,
}).sendAndConfirm(umi)
console.log('Collection:', collection.publicKey)Token Metadata (legacy, Sized Collection) — for reference:
import { createNft } from '@metaplex-foundation/mpl-token-metadata'
const collectionMint = generateSigner(umi)
await createNft(umi, {
mint: collectionMint,
name: 'My Collection',
uri: collectionMetadataUri,
sellerFeeBasisPoints: percentAmount(0),
isCollection: true,
}).sendAndConfirm(umi)Recommendation: use MPL-Core unless you need compatibility with a marketplace or tool that only supports the classic standard (rare in 2026 — Tensor, Magic Eden, and Phantom all index Core).
import { create, fetchCollection } from '@metaplex-foundation/mpl-core'
const collectionAccount = await fetchCollection(umi, collectionPubkey)
const asset = generateSigner(umi)
await create(umi, {
asset,
collection: collectionAccount,
name: 'My NFT #1',
uri: assetMetadataUri,
}).sendAndConfirm(umi)Pass the fetched collection account (not just the pubkey) to create() — it carries the updateAuthority Core needs to auto-verify the asset.
create() payer/authority, the asset is verified at mint time. No second instruction.verifyCollectionV1 instruction after mint. Until you call it, the NFT shows as "Unverified" on Magic Eden / Tensor and is filtered out of the collection.import { update } from '@metaplex-foundation/mpl-core'
await update(umi, {
asset: existingAsset,
collection: collectionAccount, // if it's in a collection
newName: 'Renamed NFT',
newUri: newMetadataUri,
}).sendAndConfirm(umi)Updates require the asset's update authority to sign. If the asset is in a collection, the collection's update authority controls updates by default.
MPL-Core's plugin system replaces the patchwork of Token Metadata extensions:
FreezeDelegate — lock an asset so it can't be transferred (staking, escrow).PermanentFreezeDelegate — collection-level immutable freeze. Set once at collection creation.Attributes — on-chain key/value pairs (separate from off-chain JSON attributes).Royalties — on-chain royalty config with a ruleSet for allow/deny lists.TransferDelegate — gasless transfers, custodial flows.Add a plugin at mint time:
await create(umi, {
asset,
collection: collectionAccount,
name: 'Soulbound Membership',
uri: metadataUri,
plugins: [
{
type: 'PermanentFreezeDelegate',
frozen: true,
authority: { type: 'None' }, // nobody can unfreeze => true soulbound
},
],
}).sendAndConfirm(umi)This is the messy one. Solana NFT royalties have never been protocol-enforced — marketplaces decide whether to honor them. The state of play in 2026:
Royalties plugin set to Allowlist mode.Practical guidance: set realistic basis points (250–500 = 2.5–5%), use the Core Royalties plugin with an allowlist of marketplaces you've verified, and don't design your revenue model around guaranteed royalties.
create() from mpl-core, and vice versa. Check the program ID if fetchCollection returns null or "Account does not exist."verifyCollectionV1. Core doesn't have this problem.imgur / your own server / a free CDN means the NFT breaks when the host disappears. Use Arweave (via Irys) or NFT.Storage / IPFS with paid pinning. Never link to a URL you don't control.await umi.rpc.airdrop() silently fails, the uploader will throw a cryptic "insufficient funds" error. Use solana airdrop 2 --url devnet from the CLI or a faucet like faucet.solana.com.properties.creators[].share values must sum to exactly 100. 99 or 101 will cause some indexers (Helius DAS, Tensor) to silently drop the creators array.See scripts/upload-asset.ts, scripts/create-collection.ts, scripts/mint-nft.ts — run in that order:
npx tsx scripts/upload-asset.ts ./assets/collection.png "My Collection" "Genesis collection"
# → prints metadataUri, save it
npx tsx scripts/create-collection.ts <collectionMetadataUri>
# → prints collection pubkey, save it
# upload the per-NFT metadata first
npx tsx scripts/upload-asset.ts ./assets/nft1.png "My NFT #1" "First mint"
npx tsx scripts/mint-nft.ts <collectionPubkey> <nftMetadataUri>
# → prints asset pubkey + Explorer link~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.