Fetch and reconstruct Solana program IDLs from on-chain accounts. Supports both Anchor IDL accounts and the Solana Program Metadata Program (PMP) end-to-end, including full historical reconstruction by replaying on-chain transactions.
| Surface | Use case |
|---|---|
npm package @solana/idl |
Import in Node services and tools |
CLI idl |
Same logic from the terminal — bare IDL, --latest, or --history |
Web + HTTP API (web/ in this repo) |
Hosted UI + JSON endpoints; live at https://idl-one.vercel.app |
npm install @solana/idl @solana/kit
# or
pnpm add @solana/idl @solana/kit@solana/kit is a peer dependency.
import { createSolanaRpc, address } from '@solana/kit';
import { fetchIdl, fetchLatestIdls, fetchAllHistories } from '@solana/idl';
const rpc = createSolanaRpc('https://api.mainnet-beta.solana.com');
const programId = address('BUYuxRfhCMWavaUWxhGtPP3ksKEDZxCD5gzknk3JfAya');
// Lean: just the current IDL (canonical PMP → fndn fallback PMP → Anchor).
// Same as GET /api/idl.
const current = await fetchIdl(rpc, programId);
if (current) console.log(current.type, current.idl);
// Rich: PMP + Anchor side-by-side with slot/time/version. Same as GET /api/latest.
const latest = await fetchLatestIdls(rpc, programId);
console.log(latest.pmp[0]?.slot, latest.anchor[0]?.slot);
// Full history of every revision, both PMP and Anchor side-by-side.
// Same as POST /api/history.
const history = await fetchAllHistories(rpc, programId);
console.log(history.pmp.length, 'PMP snapshots');
console.log(history.anchor.length, 'Anchor snapshots');For a single source only, use reconstructPmpHistory(rpc, programId, opts?) or reconstructAnchorHistory(rpc, programId) directly.
| Export | Purpose |
|---|---|
fetchIdl |
Live IDL, PMP-first with fndn fallback then Anchor fallback |
fetchAnchorIdl |
Live Anchor IDL only: { content, address } |
fetchPmpIdl |
Live PMP IDL only: { content, address, authority } (canonical then fndn) |
fetchLatestIdls |
PMP + Anchor side-by-side with version/slot/time (powers --latest) |
fetchAllHistories |
Full PMP + Anchor history side-by-side (powers --history / /api/history) |
reconstructPmpHistory |
Replay PMP transactions into a history of VirtualState snapshots |
reconstructAnchorHistory |
Replay Anchor IDL transactions into a history of snapshots |
findAnchorIdlAddress, findPmpMetadataAddress |
PDA derivation helpers |
buildPmpIdlLookups |
Enumerate PMP PDAs to try (canonical + every fndn fallback) |
IDL_FALLBACK_PMP_AUTHORITIES |
Array of non-canonical PMP authorities baked into fetchIdl / fetchPmpIdl |
Types: Idl, IdlSource, AnchorIdl, PmpIdl, PmpIdlLookup, LatestIdls, LatestIdlVersion, AllHistories, VirtualState, Snapshot, SolanaRpcClient.
The idl binary mirrors the library and has three modes, each backed by the same core function the API uses:
| Mode | Flag | Output | Backing function | API parity |
|---|---|---|---|---|
| Bare IDL (default) | (none) | Just the IDL body on stdout — pretty JSON if parsable, otherwise the raw string | fetchIdl |
GET /api/idl |
| Latest side-by-side | --latest |
{programId, pmpAddress, anchorAddress, pmp[], anchor[]} with version/slot/time for each source |
fetchLatestIdls |
GET /api/latest |
| Full history | --history |
Pretty timeline of every revision (plus optional --output / --dump-idls) |
reconstructPmpHistory / reconstructAnchorHistory |
POST /api/history |
Live IDL resolution (default and --latest) always follows the same order: canonical PMP → fndn fallback PMP → Anchor. History replay (--history) auto-detects unless you pin --type.
Parsed vs. raw IDL. Bare mode emits the IDL parsed as pretty JSON — best when you want to use the IDL (codegen, jq, inspection).
--latestand--historyemit the IDL as a raw string inside their wrapper — best when you want to record or compare it (hashing, diffing, byte-stable storage).JSON.parse↔JSON.stringifyis not guaranteed to be a byte-for-byte round trip, so the indexer-flavored modes preserve the on-chain bytes verbatim.
npx @solana/idl <program-address> [options]| Flag | Description |
|---|---|
-r, --rpc <url> |
Solana RPC URL (or set RPC_URL env var) |
-s, --seed <seed> |
Metadata seed, PMP only (default idl) |
-a, --authority <address> |
Authority address for non-canonical PMP metadata |
--latest |
Print the {programId, pmpAddress, anchorAddress, pmp[], anchor[]} payload (same shape as /api/latest) |
--history |
Replay the full IDL version history from on-chain transactions |
-t, --type <type> |
--history only. IDL type: pmp, anchor, or both (auto-detected if omitted) |
-o, --output <dir> |
--history only. Save full state snapshots to directory |
--dump-idls <dir> |
--history only. Write each distinct IDL version as JSON + an index.json timeline |
--latest and --history are mutually exclusive. The --type / --output / --dump-idls flags are rejected outside --history.
Bare IDL — just the JSON body, ready to pipe:
npx @solana/idl BUYuxRfhCMWavaUWxhGtPP3ksKEDZxCD5gzknk3JfAya \
--rpc https://api.mainnet-beta.solana.com > idl.jsonSide-by-side current view with slot + time for each source:
npx @solana/idl BUYuxRfhCMWavaUWxhGtPP3ksKEDZxCD5gzknk3JfAya \
--rpc https://api.mainnet-beta.solana.com --latestAuto-detected full history (timeline on stdout):
npx @solana/idl BUYuxRfhCMWavaUWxhGtPP3ksKEDZxCD5gzknk3JfAya \
--rpc https://api.mainnet-beta.solana.com --historyDump all distinct Anchor IDL versions to a directory:
npx @solana/idl <program> --history --type anchor --dump-idls ./idlsReconstruct both PMP and Anchor IDL history at once:
npx @solana/idl <program> --history --type both --dump-idls ./idlsWhen using --history --type both, paths for both --output and --dump-idls are automatically split into <dir>/pmp/ and <dir>/anchor/.
A Next.js UI and HTTP API live under web/. The UI exposes the same three capabilities as the API: current IDL (GET /api/idl), latest PMP + Anchor (GET /api/latest), and full history (POST /api/history). A cluster switcher (mainnet/devnet) sits in the header and is threaded through every API request. Testnet is intentionally not supported since the Program Metadata program isn't deployed there.
cd web
cp .env.example .env.local # set RPC_MAINNET / RPC_DEVNET
pnpm install
pnpm run dev # http://localhost:3000Deploy to Vercel by setting the project root directory to web and adding RPC_MAINNET and/or RPC_DEVNET in the environment. A legacy RPC_URL is still honored as a fallback for mainnet-beta only.
All routes accept a cluster parameter (mainnet-beta (default) or devnet). GET routes take it as a query parameter; POST /api/history accepts it in the JSON body. A request to a cluster whose env var is unset returns 500 naming the missing variable.
/api/idl and /api/latest are quick reads. /api/history is the heavy one — it reconstructs every revision from on-chain transactions and is configured with a 300s function timeout (capped by your Vercel plan: 60s on Hobby, 300s on Pro). For very long deploy histories, the CLI against a private RPC is the more reliable path.
GET /api/idl?programId=<address>&cluster=<cluster> — Returns the current IDL (canonical PMP, then non-canonical PMP via the fallback authority, then Anchor):
{
"programId": "BUYux…",
"type": "pmp",
"idl": {}
}type is "pmp" or "anchor". idl is JSON-parsed when possible, otherwise returned as a string. Returns 400 for a missing or invalid programId / cluster, 404 when neither source has an IDL, 500 when the cluster's RPC env var is unset.
GET /api/latest?programId=<address>&cluster=<cluster> — Returns both current sources side by side (when present): derived pmpAddress, anchorAddress, and two arrays pmp and anchor, each with at most one entry including decoded version metadata and the full content string for the live IDL.
contentis kept as the raw on-chain string (not parsed) on this endpoint and on/api/history— same reasoning as the CLI's--latest/--historymodes (byte-stable hashing and diffing for indexers).GET /api/idlis the parsed/usable view.
GET /api/history?programId=<address>&cluster=<cluster> and POST /api/history — Reconstructs distinct IDL versions over time. Both methods accept the same inputs and return the same shape; pick whichever fits your client. POST takes a JSON body { "programId": "<address>", "cluster": "<cluster>" }. Responses are sent with Cache-Control: no-store.
Each of pmp and anchor is an array of objects with type, version, slot, time, activeFrom, activeTo ("current" or { "slot", "time" }), and the content string for that revision. Either array may be empty if that format has no on-chain history.
The history APIs (reconstructAnchorHistory / reconstructPmpHistory) replay every on-chain transaction that touched the program's IDL metadata account (and related buffer accounts) and apply each relevant instruction to a virtual state.
- Anchor: legacy IDL instructions and Anchor 0.30+ instructions —
Create,CreateBuffer,Write,SetBuffer,SetAuthority,Close, and theidl_*variants. Buffer payloads are reconstructed by replaying writes to those accounts. - PMP: SPL Program Metadata instructions —
Allocate,Write,Initialize,SetData,SetAuthority,SetImmutable,Trim,Close,Extend.
The live paths (fetchIdl / fetchLatestIdls) skip replay and read live chain state, so they are dramatically cheaper than a full history scan.
fetchIdl (and the bare CLI mode) resolve in this order:
- Canonical PMP with the requested seed (default
idl). - Non-canonical PMP for every entry of
IDL_FALLBACK_PMP_AUTHORITIES(currently justfndnu15…). - Anchor IDL account.
Returns null if none resolves.
pnpm install
pnpm test # unit + offline integration (recorded fixtures, via vitest)
pnpm run build # dual ESM + CJS bundles via tsup, .d.ts via tsc
pnpm run typecheckThe build emits both dist/index.js (ESM) and dist/index.cjs (CJS) plus type
declarations, matching the rest of the Solana ecosystem (@solana/kit,
@solana-program/*). Consumers using require() (Node CJS, tsx in a CJS
project, ts-jest, etc.) and import (ESM, modern bundlers) both resolve
through the package exports map.
Integration tests run against recorded fixtures in __tests__/fixtures/<program>-<cluster>/ — every RPC response the production code paths need is serialized to disk, so the suite is hermetic and offline. To refresh or add fixtures (requires RPC_MAINNET / RPC_DEVNET or web/.env.local):
pnpm run record:fixtures BUYuxRfhCMWavaUWxhGtPP3ksKEDZxCD5gzknk3JfAya mainnet-beta
pnpm run record:fixtures TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA devnetThe recorder reuses any fixture already on disk, so reruns only fetch what's missing.
pnpm run test:integration # only the integration suite (fixture-backed, offline)MIT — see LICENSE.