Imported from phantom/phantom-skill-generator (
skills/generate-skill/SKILL.md). Install upstream withnpx skills add phantom/phantom-skill-generator --skill generate-skill. Copyright stays with the author.
Solana Skill Generator
Generates a skill file from any Solana Anchor program with a published IDL. The generated skill gives AI agents the ability to interact with that program's instructions using Phantom MCP for signing and sending transactions.
When to Use
When a developer wants to build an MCP skill for a Solana program and says something like:
- "generate a skill for [program address]"
- "create a skill from this program"
- "run generate-skill for [address]"
How to Run
Step 1 — Get the program address
If the user hasn't provided a program address, ask for it. Accept mainnet program addresses only (for v1).
Step 2 — Fetch the IDL
Run these commands in sequence using Bash. The setup step is self-contained — it creates an isolated temp directory with all dependencies so this works in any session.
Setup (run once per session):
mkdir -p /tmp/phantom-idl-fetch && cd /tmp/phantom-idl-fetch && echo '{"type":"module"}' > package.json && npm install @coral-xyz/anchor@0.29.0 @coral-xyz/anchor-new@npm:@coral-xyz/anchor@latest @solana/web3.js @solana/spl-token --save-quiet && echo "Dependencies ready"
Why two anchor versions? No single version handles both IDL formats:
@coral-xyz/anchor@0.29.0— works with legacy IDLs (Anchor < 0.30) but crashes on new-format IDLs@coral-xyz/anchor-new(alias for latest) — works with new IDLs (Anchor ≥ 0.30) but crashes on legacy IDLsThe fetch script uses 0.29.0 (its
fetchIdlworks for both). After fetching, detect the format and use the right package in generated build scripts.
Fetch script — save as /tmp/phantom-idl-fetch/fetch-idl.mjs:
import { Program } from "@coral-xyz/anchor";
import { Connection, PublicKey, clusterApiUrl } from "@solana/web3.js";
const PROGRAM_ADDRESS = process.argv[2];
if (!PROGRAM_ADDRESS) { console.error("Usage: node fetch-idl.mjs <program_address>"); process.exit(1); }
const connection = new Connection(clusterApiUrl("mainnet-beta"), "confirmed");
const programId = new PublicKey(PROGRAM_ADDRESS);
// Program.fetchIdl uses the legacy anchor:idl on-chain storage (createWithSeed).
// Most published Anchor IDLs use this format regardless of Anchor version.
const idl = await Program.fetchIdl(programId, { connection });
if (!idl) {
console.log("NO_IDL_FOUND");
process.exit(0);
}
console.log(JSON.stringify(idl, null, 2));
Run it:
cd /tmp/phantom-idl-fetch && node fetch-idl.mjs <PROGRAM_ADDRESS>
If NO_IDL_FOUND: Tell the developer clearly — "No IDL found for this program. This usually means it's a native program or an Anchor program that hasn't published its IDL on-chain. Programs without a published IDL are not supported in v1." Do not continue.
Step 3 — Check for Phantom MCP and catalog available tools
Check whether get_connection_status (from @phantom/mcp-server) is available as a tool in the current session.
- If not available: Include a prominent setup block in the generated skill recommending Phantom MCP installation.
- If available: Note which tool categories are present so the generated skill uses the right tool for each instruction type.
Phantom MCP exposes 27 tools. Generated skills should prefer the highest-level tool that fits the instruction rather than always building raw transactions. Use this reference when deciding what to generate:
Always use (every action):
| Tool | When |
|---|---|
get_connection_status |
First — confirm the user is authenticated before doing anything |
get_wallet_addresses |
Get the user's Solana (and EVM) address — fee payer and default signer |
simulate_transaction |
Before every send_solana_transaction — catch errors before they hit chain |
Prefer over raw transactions when the instruction maps cleanly:
| Tool | Use instead of a raw tx when... |
|---|---|
transfer_tokens |
The instruction is a simple SOL or SPL token transfer |
buy_token |
The instruction is a token swap on Solana |
pay_api_access |
Collecting a CASH payment from the user |
sign_solana_message |
The instruction only requires message signing, not a transaction |
send_evm_transaction |
The program targets an EVM chain (Base, Polygon, Ethereum, Monad) |
sign_evm_personal_message |
EVM personal message signing |
sign_evm_typed_data |
EVM typed data (EIP-712) signing |
Read before acting (use to inform the user or validate inputs):
| Tool | Use when... |
|---|---|
get_token_balances |
Instruction involves spending tokens — verify the user has enough first |
get_token_allowance |
EVM instruction requires a token approval check |
get_perp_account |
Instruction interacts with a perp account — fetch current state first |
get_perp_markets |
User needs to pick a market — list available ones |
get_perp_positions |
Show open positions before modify/close actions |
get_perp_orders |
Show open orders before cancel actions |
get_perp_trade_history |
User asks about past trades |
Perp-specific (use directly for perpetuals programs — do not build raw txs for these):
| Tool | Use for... |
|---|---|
open_perp_position |
Opening a leveraged long/short |
close_perp_position |
Closing an open position |
cancel_perp_order |
Cancelling a limit order |
update_perp_leverage |
Changing leverage on a position |
deposit_to_hyperliquid |
Depositing collateral |
transfer_spot_to_perps |
Moving tokens from spot to perp margin |
withdraw_from_perps |
Withdrawing collateral |
portfolio_rebalance |
Rebalancing a perp portfolio |
Auth:
| Tool | Use when... |
|---|---|
phantom_login |
User is not authenticated — trigger the login flow |
Step 4 — Parse the IDL
Anchor has two IDL formats. Detect which one you have before parsing:
Legacy format (Anchor < 0.30) — top-level name field:
{ "version": "0.1.0", "name": "my_program", "instructions": [...] }
New format (Anchor 0.30+) — top-level address and metadata fields:
{ "address": "ABC...", "metadata": { "name": "my_program", "version": "0.2.0" }, "instructions": [...] }
Extract the following, handling both formats:
| Field | Legacy | New format |
|---|---|---|
| Program name | idl.name |
idl.metadata.name |
| Version | idl.version |
idl.metadata.version |
| Program address | use the address passed by the user | idl.address |
For each instruction, extract:
name— instruction nameargs— list of{ name, type }parameters- Legacy:
args[].typeis a string or object - New format:
args[].typefollows the same shape but may use{ defined: { name: "TypeName" } }instead of{ defined: "TypeName" }
- Legacy:
accounts— list of{ name, writable?, signer?, pda? }- Legacy uses
isMut/isSigner; new format useswritable/signer - Normalize to
writable/signerwhen generating code
- Legacy uses
docs— description if present (array of strings — join with space)
Account filtering — exclude from user-facing docs (but keep in code): Skip accounts that match these patterns (they are boilerplate):
systemProgram,tokenProgram,associatedTokenProgramrent,clock,sysvarInstructionsprogramId, any account ending inProgram- PDAs derived by the program itself (where
pdais defined in the IDL)
Type mapping — convert Anchor types to human-readable:
| Anchor | Description |
|---|---|
u64, u128, i64 |
number (use lamports/basis points as appropriate) |
publicKey / pubkey |
Solana wallet or token account address |
bool |
true/false |
string |
text |
{ defined: "TypeName" } or { defined: { name: "TypeName" } } |
reference the IDL's types array to expand |
{ vec: T } |
array of T |
{ option: T } |
optional T |
Step 5 — Generate the skill file
Write the skill file to <skills-dir>/<program-name>/SKILL.md in the current working directory. Create <skills-dir>/<program-name>/ if it doesn't exist. If the platform-specific skills directory is unknown, use skills/ as the default.
For the program name use idl.name (legacy) or idl.metadata.name (new format); for the address use idl.address (new format) or the address passed by the user (legacy). Use this structure:
---
name: <program-name-kebab-case>
description: Interact with <Program Name> on Solana. <One sentence from IDL metadata or derived from program name.>
---
# <Program Name> Skill
Enables AI agents to interact with [<Program Name>](<explorer link>) (program: `<ADDRESS>`).
<If Phantom MCP NOT available in session:>
## ⚠️ Setup Required: Phantom MCP
This skill uses Phantom MCP to sign and send transactions. Install it before using this skill:
**Add to your MCP config (platform-specific path, e.g. `claude_desktop_config.json`):**
```json
{
"mcpServers": {
"phantom": {
"command": "npx",
"args": ["-y", "@phantom/mcp-server@latest"]
}
}
}
On first use, you'll be prompted to authenticate with Google or Apple.
Helper: Fetch Pool State (if needed)
If any instruction requires PDAs or accounts that can be derived from on-chain state (e.g. vaults, oracles, tick arrays), add a ## Helper section before the actions with a script that fetches the program's state account and derives those addresses. This removes the need for the developer to look them up manually.
Pattern (use @coral-xyz/anchor-new for new-format IDLs, @coral-xyz/anchor for legacy):
// Run with: node /tmp/phantom-idl-fetch/<program>-accounts.mjs <STATE_OR_POOL_ADDRESS>
import anchor from "@coral-xyz/anchor-new"; // or "@coral-xyz/anchor" for legacy IDLs
const { Program, AnchorProvider } = anchor;
import { Connection, PublicKey, clusterApiUrl } from "@solana/web3.js";
const PROGRAM_ID = new PublicKey("<PROGRAM_ID>");
const connection = new Connection(clusterApiUrl("mainnet-beta"), "confirmed");
const provider = new AnchorProvider(connection, { publicKey: PublicKey.default }, {});
const idl = await Program.fetchIdl(PROGRAM_ID, provider);
const program = new Program(idl, provider); // new format: (idl, provider), legacy: (idl, PROGRAM_ID, provider)
const state = await program.account.<stateName>.fetch(new PublicKey(process.argv[2]));
console.log(JSON.stringify({ /* relevant derived fields */ }, null, 2));
Only add this section if the IDL has accounts that are clearly derivable (vaults, PDAs with known seeds, state-owned accounts). If all required accounts must be supplied by the user, omit this section.
Available Actions
<For each instruction, one section:>
<docs string if present, or a generated one-sentence description based on instruction name and accounts>
What to ask the user for:
<arg name>: —<account name>: Solana address —
To execute:
Before writing the execution steps, check the tool reference table in Step 3. If this instruction maps to a high-level Phantom MCP tool (
transfer_tokens,buy_token,open_perp_position, etc.), use that tool directly and skip the raw transaction script. Only build a raw transaction when no high-level tool fits.
If using a high-level Phantom MCP tool:
- Call
get_connection_status— if not authenticated, callphantom_loginfirst. - Call
get_wallet_addressesto get the user's Solana address. - If the instruction spends tokens, call
get_token_balancesand confirm the user has sufficient balance before proceeding. - Call the appropriate tool directly (e.g.
transfer_tokens,buy_token,open_perp_position).
If building a raw Anchor transaction:
- Call
get_connection_status— if not authenticated, callphantom_loginfirst. - Call
get_wallet_addressesto get the user's Solana address. - If the instruction spends tokens, call
get_token_balancesand confirm sufficient balance before proceeding. - Ensure setup has been run (see Step 2 of this skill). Save and run the script from
/tmp/phantom-idl-fetch/so it picks up the installed dependencies.
Choose the right anchor import based on IDL format:
idl.addressexists → new format →import anchor from "@coral-xyz/anchor-new"andnew Program(idl, provider)idl.nameat top level → legacy format →import anchor from "@coral-xyz/anchor"andnew Program(idl, PROGRAM_ID, provider)
// Save as /tmp/phantom-idl-fetch/build-<instruction>.mjs
// Run: node /tmp/phantom-idl-fetch/build-<instruction>.mjs <USER_WALLET> <args...>
import anchor from "@coral-xyz/anchor-new"; // ← use "@coral-xyz/anchor" for legacy IDLs
const { Program, AnchorProvider, BN } = anchor;
import { Connection, PublicKey, clusterApiUrl } from "@solana/web3.js";
const PROGRAM_ID = new PublicKey("<ADDRESS>");
const USER_WALLET = new PublicKey(process.argv[2]);
// <additional args: process.argv[3], process.argv[4], ...>
const connection = new Connection(clusterApiUrl("mainnet-beta"), "confirmed");
const provider = new AnchorProvider(connection, { publicKey: USER_WALLET }, {});
const idl = await Program.fetchIdl(PROGRAM_ID, provider);
const program = new Program(idl, provider); // ← legacy: new Program(idl, PROGRAM_ID, provider)
const tx = await program.methods
.<instructionName>(<u64/i64 args use new BN(value), pubkey args use new PublicKey(arg)>)
.accounts({
<all accounts mapped out>
})
.transaction();
// If the flow needs a user ATA that may not exist yet, create it first:
// import { getAssociatedTokenAddress, createAssociatedTokenAccountInstruction } from "@solana/spl-token";
// const ata = await getAssociatedTokenAddress(tokenMint, USER_WALLET);
// tx.instructions.unshift(createAssociatedTokenAccountInstruction(USER_WALLET, ata, USER_WALLET, tokenMint));
tx.recentBlockhash = (await connection.getLatestBlockhash()).blockhash;
tx.feePayer = USER_WALLET;
console.log(tx.serialize({ requireAllSignatures: false }).toString("base64"));
- Send the transaction (Phantom simulates automatically and shows a preview first):
send_solana_transaction({
transaction: "<base64 output from script>"
})
The first call (without confirmed: true) runs a simulation and returns expected asset changes. Show the user the preview, then call again with confirmed: true to actually send:
send_solana_transaction({
transaction: "<base64 output from script>",
confirmed: true
})
Adding Payments with CASH
To charge users for skill usage, gate any action behind a CASH payment:
- Call your backend/paywall endpoint to receive a prepared CASH payment transaction (base64).
- Call
pay_api_access({ preparedTx: "<base64>" }). - Only proceed to the instruction if payment succeeds.
pay_api_access({
preparedTx: "<base64 prepared CASH payment transaction>"
})
---
### Step 6 — Tell the developer what to do next
After writing the file, give a short summary:
Generated skill: //SKILL.md
- tools:
- Phantom MCP: <"detected in session" or "setup instructions included in skill">
Next steps:
- Review the generated accounts in each tool — some may need real addresses (token mints, PDAs).
- Enrich the descriptions if you want clearer agent behavior.
- To charge for skill usage, see the CASH section at the bottom of the skill.
- Share your skill using your platform's distribution flow (git repo, package manager, or direct file copy).
## Rules
- Never fabricate account addresses. Use placeholders like `"<FILL: mSOL mint address>"` for accounts not derivable from the IDL.
- Never reference a skill marketplace, registry, or publishing pipeline that doesn't exist.
- Keep descriptions grounded in what the IDL actually says. If there are no docs, use the instruction name to infer purpose — don't invent behavior.
- System and boilerplate accounts go in the code but not in the "ask the user for" section.