---
name: generate-skill
description: Generate a reusable skill from a Solana Anchor program IDL. Takes a program address, fetches the on-chain IDL, and outputs a ready-to-use skill with Phantom MCP integration for transaction execution.
---

# 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):**
```bash
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 IDLs
>
> The fetch script uses 0.29.0 (its `fetchIdl` works 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`:**
```javascript
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:**
```bash
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:
```json
{ "version": "0.1.0", "name": "my_program", "instructions": [...] }
```

**New format** (Anchor 0.30+) — top-level `address` and `metadata` fields:
```json
{ "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 name
- `args` — list of `{ name, type }` parameters
  - Legacy: `args[].type` is a string or object
  - New format: `args[].type` follows the same shape but may use `{ defined: { name: "TypeName" } }` instead of `{ defined: "TypeName" }`
- `accounts` — list of `{ name, writable?, signer?, pda? }`
  - Legacy uses `isMut` / `isSigner`; new format uses `writable` / `signer`
  - Normalize to `writable` / `signer` when generating code
- `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`, `associatedTokenProgram`
- `rent`, `clock`, `sysvarInstructions`
- `programId`, any account ending in `Program`
- PDAs derived by the program itself (where `pda` is 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:

---

```markdown
---
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.

<End Phantom MCP block>

## 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):
```javascript
// 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:>

### <instruction name in Title Case>

<docs string if present, or a generated one-sentence description based on instruction name and accounts>

**What to ask the user for:**
<List only the non-filtered args and non-system accounts the user must supply. Use plain language.>
- `<arg name>`: <type description> — <what it means>
- `<account name>`: Solana address — <what this account is>

**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:**

1. Call `get_connection_status` — if not authenticated, call `phantom_login` first.
2. Call `get_wallet_addresses` to get the user's Solana address.
3. If the instruction spends tokens, call `get_token_balances` and confirm the user has sufficient balance before proceeding.
4. Call the appropriate tool directly (e.g. `transfer_tokens`, `buy_token`, `open_perp_position`).

**If building a raw Anchor transaction:**

1. Call `get_connection_status` — if not authenticated, call `phantom_login` first.
2. Call `get_wallet_addresses` to get the user's Solana address.
3. If the instruction spends tokens, call `get_token_balances` and confirm sufficient balance before proceeding.
4. 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.address` exists → **new format** → `import anchor from "@coral-xyz/anchor-new"` and `new Program(idl, provider)`
- `idl.name` at top level → **legacy format** → `import anchor from "@coral-xyz/anchor"` and `new Program(idl, PROGRAM_ID, provider)`

```javascript
// 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"));
```

5. Send the transaction (Phantom simulates automatically and shows a preview first):

```javascript
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:

```javascript
send_solana_transaction({
  transaction: "<base64 output from script>",
  confirmed: true
})
```

<End instruction section>

---

## Adding Payments with CASH

To charge users for skill usage, gate any action behind a CASH payment:

1. Call your backend/paywall endpoint to receive a prepared CASH payment transaction (base64).
2. Call `pay_api_access({ preparedTx: "<base64>" })`.
3. Only proceed to the instruction if payment succeeds.

```javascript
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: <skills-dir>/<name>/SKILL.md
- <N> tools: <comma-separated instruction names>
- Phantom MCP: <"detected in session" or "setup instructions included in skill">

Next steps:
1. Review the generated accounts in each tool — some may need real addresses (token mints, PDAs).
2. Enrich the descriptions if you want clearer agent behavior.
3. To charge for skill usage, see the CASH section at the bottom of the skill.
4. 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.
