For AI agents: the documentation index is at /llms.txt. Markdown versions of pages are available by appending .md to the URL.
Skip to main content

Decoding Instructions

Solana instruction data is a packed Borsh byte string with no self-describing structure: unlike an EVM log, there's no ABI travelling with it. HyperIndex needs to know which bytes identify an instruction and how to read the rest, and there are two ways to tell it:

  • Point at an IDL. The usual path, and nothing else on this page applies.
  • Declare the layout yourself. For programs with no IDL - most native ones.

Point at an IDL

Give the program an IDL JSON file (relative to config.yaml). Anchor 0.30+, legacy Anchor, Shank and Codama IDLs all work through the same path:

programs:
- name: Jupiter
program_id: JUP6LkbZbjS1jKKwapdHNy74zcZ3tLUZoi5QNyVTaV4
idl: ./idls/jupiter.json

That's the whole configuration. Every usable instruction the IDL declares is indexable - names, discriminators, argument layouts, ordered account names (including nested account groups) and the IDL's types registry all come from the file - so you select what you index by name in onInstruction and read instruction.args and instruction.accounts typed after codegen.

Add an instructions row only to override or extend the catalog:

  • A row whose name the IDL declares replaces that instruction, and must spell out both accounts and args.
  • A row with a new name adds an instruction the IDL didn't declare.

The argument type table below describes what the IDL's types become in TypeScript. Everything else on this page is about programs without an IDL.

Programs without an IDL

For a program with no IDL - most native programs - you declare the pieces an IDL would have carried: which bytes select the instruction, what its arguments look like, and what to call its accounts.

Discriminators

The discriminator is a 0x-prefixed hex prefix of the instruction data, of any whole number of bytes. Every instruction whose data carries the prefix is dispatched to it.

  • Format: hex only, with the 0x prefix. Base58 and decimal are not accepted (base58 is only for program_id).
  • Native / non-Anchor programs: whatever leading byte(s) the program uses (SPL Token transfer is 0x03; Raydium AMM v4 swap is 0x09).
  • Anchor programs: the 8-byte Anchor sighash, which an idl carries for you.
  • The whole program: "0x" - the empty prefix, carried by every call.
instructions:
- name: swap
discriminator: "0x09" # 1-byte native
- name: sharedAccountsRoute
discriminator: "0xc1209b3341d69c81" # 8-byte Anchor sighash

instruction.discriminator in a handler reads back the same string you wrote in config.yaml, so a config value and a handler comparison always match.

Overlapping prefixes all fire

Dispatch is by prefix, not by exclusive match, so an entry whose prefix a call carries always receives it. A program-wide "0x" entry fires alongside a keyed one, and two entries may deliberately share a prefix - for example the layouts before and after a program upgrade. Each decodes with its own args, and one whose layout rejects the data is skipped for that call.

Declaring the layout

args is the Borsh argument list in order (after the discriminator); accounts names the positional account slots.

programs:
- name: Raydium
program_id: 675kPX9MHTjS2zt1qfr1NYHuzeLXfQM9H24wFSUt1Mp8
instructions:
- name: swap
discriminator: "0x09"
accounts: # positional: slot 0 is tokenProgram, etc.
- tokenProgram
- amm
- userSourceTokenAccount
- userDestTokenAccount
args:
- { name: amountIn, type: u64 }
- { name: minAmountOut, type: u64 }

Account slots also accept ?optional (the key is absent when the call omits the slot or fills it with the program id) and _ (holds a position without naming it). See account slots.

Verify positional layouts

For native programs the account order is the program's canonical layout, not something HyperIndex can verify. Check it against a real transaction. Accounts beyond your named list still arrive, positionally, in instruction.accountArguments.

args is also a filter

Setting args attaches a decoder, and a call whose data the layout rejects is skipped rather than delivered undecoded. That makes the three states meaningfully different:

argsBehavior
omittedNo decoder at all. Every matched call is indexed and the payload stays raw, reachable as instruction.data.
[]The instruction takes no arguments, so only calls carrying nothing past the discriminator are indexed.
a listOnly calls whose data decodes cleanly against the layout are indexed.

An idl always declares the layout of the instructions it names, empty included.

Supported argument types

These are the types you can write in args, and also what HyperIndex understands from an IDL. The right column is the TypeScript type each value has in instruction.args after codegen.

typeType in instruction.args
boolboolean
u8 u16 u32, i8 i16 i32, f32 f64number
u64 u128 i64 i128bigint
stringstring
pubkey (alias publicKey)string (base58)
bytesUint8Array
{ option: <type> }the value, or null
{ vec: <type> }readonly T[] - but vec<u8> is a Uint8Array
{ array: [<type>, <len>] }a readonly N-tuple - but [u8, N] is a Uint8Array
{ struct: [ {name,type}, … ] }object
{ enum: [ {name, fields?}, … ] }a variant name as a string literal, or { VariantName: { …fields } } for a variant with fields
args:
- { name: amount, type: u64 } # bigint
- { name: authority, type: pubkey } # base58 string
- { name: maybeOwner, type: { option: pubkey } } # string | null
- { name: seedHash, type: { array: [u8, 32] } } # Uint8Array
- { name: side, type: { enum: [{ name: Bid }, { name: Ask }] } } # "Bid" | "Ask"

Nominal types are declared inline at the field that uses them. There's no way to name one in YAML and refer to it elsewhere - attach an idl to the program when its types are shared between instructions.

Two things worth knowing about the decoded output:

  • Byte arrays are Uint8Array. bytes, vec<u8> and [u8; N] all decode to Uint8Array, so a 32-byte hash doesn't come back as a base58 string.
  • Address lookup tables need no configuration. ALT-resolved addresses arrive in the instruction's account list and are mapped positionally like any other.