
Short answer: recover the program ID from an unsigned transaction, then search on-chain metadata, GitHub, published SDKs, npm packages, and the app’s frontend. For Anchor programs, try IDLGuesser against the deployed binary. If none of those produces a complete IDL, record every app interaction with a fresh wallet and reconstruct the remaining interface from the resulting transactions. Whatever you find, validate it against real instruction data.
IDL stands for Interface Description Language. It is a machine-readable description of a program's instructions, accounts, and data types. An IDL can be generated for any Solana program, and frameworks like Anchor will usually generate one for you.
But not every program developer publishes it. That makes it harder for anyone else to decode transactions, build a client, or integrate with the program. Fortunately, unpublished does not mean unrecoverable.
You have the app, its JavaScript, and the deployed binary. How much of the IDL can you recover?
In 2024 I posted a quick trick: open Chrome DevTools, search every loaded source for JSON.parse('{", and copy the JSON. The original technique worked because the app had placed its complete IDL in a public JavaScript bundle.
That tweet was already about the case where nothing useful was published on-chain. A JavaScript bundle is off-chain, after all. This article expands that trick into a longer fallback chain for when the complete IDL is not sitting in one obvious place.
You might not know which program the app calls. That is fine. Ask the app to build a real transaction, but stop at the wallet approval screen. Do not sign or submit it.
Some wallets let you inspect or copy the raw serialized transaction. If yours does, copy the base64 transaction. If it does not, open DevTools and pause immediately before the app calls the wallet’s signTransaction, signAllTransactions, or sendTransaction method. The transaction object will be available in the paused scope.
Paste the serialized transaction into Solana Explorer's Transaction Inspector. It decodes the message for you and shows the top-level instructions, their program IDs, account keys, and raw instruction data. Make sure the inspector is set to the correct cluster. This matters especially for version 0 transactions that use Address Lookup Tables (versioned transaction format).
Why is this enough? Every compiled Solana instruction identifies the program that will execute it and includes the raw discriminator and serialized arguments (Solana transaction structure). The inspector simply turns that binary structure into something readable.
A transaction can call several programs. You will often see the Compute Budget Program, System Program, token programs, or Associated Token Program beside the target. Inspect each compiled instruction and look for the unfamiliar program whose instruction data matches the action you triggered.
This only identifies programs invoked by the top-level instructions. A program reached later through CPI is not marked as an invoked program in the unsigned transaction. Simulation logs or an executed transaction are needed to see that call tree.
With the program ID in hand, do this simple check first, in case the IDL was published on-chain. @solana/idl checks Program Metadata storage and the legacy Anchor IDL account:
npx @solana/idl "$PROGRAM_ID" --rpc "$RPC_URL" > published.json
Anchor's legacy IDL storage is implemented through extra instructions built into the program. We covered how those instructions work, and their security implications, in Hidden IDL Instructions and How To Abuse Them.
If that returns an IDL, you are done. Everything below assumes it finds nothing.
Before reverse-engineering the frontend or binary, check whether the protocol has published an SDK, TypeScript client, npm package, source repository, generated client, or any other developer bundle. The website may not expose an IDL directly while the official client package quietly ships one.
Also paste the exact program ID into GitHub's global code search. Program IDs are distinctive enough that this can lead directly to an IDL, source repository, SDK, test, or integration written by somebody else.
Do not assume an IDL is legitimate just because you found it on GitHub. It may be stale, built for another deployment, or deliberately malicious. Malicious Solana IDLs: when depositFunds calls withdrawFunds shows why a plausible-looking IDL must be treated as a lead rather than ground truth.
Download the package instead of relying on its documentation page. For npm packages, npm pack gives you the exact tarball users install:
PACKAGE_TARBALL=$(npm pack @scope/package --silent)
tar -xf "$PACKAGE_TARBALL"
rg -n '"instructions"|"accounts"|"discriminator"|PROGRAM_ID' package
find package -iname '*idl*' -o -iname '*.json'
Search both the source and built output. The IDL may be a JSON file, an exported TypeScript object, generated client code, or an escaped string inside dist/. Also search for the program ID and distinctive instruction names. Even when there is no complete IDL, generated clients often reveal discriminators, argument encoders, and account order.
Now search the frontend. The old JSON.parse('{" query is still worth ten seconds, but it depends on one bundler representation. A more useful first search is the program ID you just recovered.
Command+Option+F on macOS or Control+Shift+F on Windows and Linux.instructions, accounts, args, and discriminator.Chrome’s Search panel scans loaded resources and opens matches in Sources (DevTools Search). Sometimes the whole IDL is sitting there as an object literal or escaped JSON. Copy it and validate it.
Minification can make the source unpleasant without removing the runtime object. If the app uses Anchor’s TypeScript client, it eventually constructs a Program from an IDL. Set a line breakpoint where the Program or instruction coder is created, reload or trigger the action again, then inspect the paused local values.
From the Console:
copy(JSON.stringify(idlValue, null, 2))
The variable may be called e, t, or something equally helpful. Look at the object shape. Chrome exposes both line breakpoints and the copy() helper in DevTools (breakpoints, Console utilities).
The runtime breakpoint is the main improvement over my old tweet. It follows the value the app consumes instead of guessing how the bundler encoded it.
Some clients build instructions manually. They ship the discriminator, ordered account metas, and encoders needed by the UI, but never ship one complete IDL object. Searching harder will not create an object that is not there.
For Anchor programs, use Sec3’s IDLGuesser:
git clone https://github.com/sec3-service/IDLGuesser.git
cd IDLGuesser
cargo build --release
./target/release/idl-guesser \
--url "$RPC_URL" \
--output recovered.json \
"$PROGRAM_ID"
IDLGuesser downloads the deployed program binary and looks for patterns produced by Anchor’s macros. According to Sec3’s technical write-up, it can recover instruction names and discriminators, ordered accounts, account names, signer and writable flags, common constraints, and some argument or account types.
The mechanism is useful because Anchor leaves regular structures in the binary. Instruction handlers log their names. Generated try_accounts functions process accounts in order and branch to recognizable constraint errors. IDLGuesser follows those patterns and writes IDL-shaped JSON.
But it is still guessing. Original parameter names are usually gone, optimized argument deserialization is difficult to follow, and complex optional or nested account contexts can confuse the analysis. The generated output may use names such as field_0, which is honest.
Use --force-guess when an old public IDL exists and you specifically want the binary-derived result.
If there is no findable IDL and IDLGuesser does not work, make the app produce the evidence yourself.
Create a fresh wallet specifically for this app and fund it only with what you are prepared to use for testing. Then perform every on-chain interaction the app exposes. As you go, keep a simple interaction log: what you clicked, the values you entered, the time, and the resulting transaction signature. Try the same action with different values where practical. One transaction is rarely enough to distinguish integer widths, flags, enums, optional fields, and lengths that all happened to fit the first sample.
Download the complete transactions for that wallet, including account keys, raw instruction data, signer and writable flags, inner instructions, return data, and program logs. Give those transactions, the target program ID, and your interaction log to an AI agent. Tell it to correlate each UI action with the corresponding instruction and rebuild an IDL, marking every inferred name or type with its confidence and listing any fields it cannot explain.
This is still manual reverse engineering, even if an agent does most of the byte matching. The fresh wallet gives you a clean transaction history, while the notes give semantic meaning to otherwise anonymous discriminators and byte ranges. The result is a hypothesis that still needs to be validated against transactions the agent did not use while constructing it.
Do not trust a recovered IDL because it parses as JSON. The same applies to an IDL downloaded from a package, repository, or random link. Treat it as untrusted input until it matches the program's actual wire format.
Check it against several transactions:
The serialized transaction you copied at the start is useful twice. It reveals the program ID, then becomes the first test vector for the recovered interface.
If an official SDK, package, or browser bundle contains the full IDL, take that path, but verify it. If it only contains a partial client, combine those pieces with IDLGuesser and real transactions. If none of that works, build a clean transaction corpus and reconstruct the missing interface from observed behavior. At that point the word “secret” is doing a lot of work.
If you are building on Solana and need a security audit, talk to Accretion. We only audit Solana.