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 & IDLs

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. To turn raw bytes into typed args and named accounts, HyperIndex needs two things per instruction:

  1. A discriminator — the leading bytes that identify which instruction it is.
  2. A schema — the argument layout and account names, from an Anchor IDL, an inline declaration, or a bundled schema.

Discriminators

The discriminator is the first N bytes of the instruction data. HyperIndex matches the leading bytes of every instruction in the matched program against the discriminators you configured; the longest configured length is tried first, so a program can mix 8-byte Anchor and 1-byte native instructions unambiguously.

  • Format: hex only, with optional 0x prefix. Base58 and decimal are not accepted here (base58 is only for program_id).
  • Length: exactly 1, 2, 4, or 8 bytes (2, 4, 8, or 16 hex digits).
  • Anchor programs: the discriminator is the 8-byte Anchor sighash — sha256("global:" + snake_case(instructionName))[0..8].
  • Native / non-Anchor programs: whatever leading byte(s) the program uses (e.g. SPL Token Transfer is 0x03; Raydium AMM v4 swap is 0x09).
instructions:
- name: sharedAccountsRoute
discriminator: "0xc1209b3341d69c81" # 8-byte Anchor sighash
- name: swap
discriminator: "0x09" # 1-byte native
Always set discriminator, even with an IDL

HyperIndex reads the discriminator only from config.yaml. Modern Anchor IDLs (0.30+) embed a discriminator array, but codegen ignores it, and it is the configured bytes that the IDL's argument/account layout is looked up by.

Omit discriminator and two things happen at once: the instruction matches every instruction of the program, and no layout resolves, so params is undefined on every one of them. Neither failure raises an error.

Computing an Anchor discriminator

For an Anchor program, compute the 8-byte sighash from the instruction name:

import { createHash } from "node:crypto";

function anchorDiscriminator(name: string): string {
const snake = name.replace(/([a-z0-9])([A-Z])/g, "$1_$2").toLowerCase();
const hash = createHash("sha256").update(`global:${snake}`).digest();
return "0x" + hash.subarray(0, 8).toString("hex");
}

anchorDiscriminator("route"); // 0xe517cb977ae3ad2a
anchorDiscriminator("sharedAccountsRoute"); // 0xc1209b3341d69c81

Where the schema comes from

HyperIndex resolves an instruction's argument/account layout in this order:

SourceWhenHow to use
Inline schemaInstruction has both args and accountsYou declare the layout directly. Highest priority; mutually exclusive with the program's idl.
Anchor IDLProgram has idl: setHyperIndex derives args + accounts from the IDL entry whose discriminator matches the instruction's configured discriminator.
BundledProgram id matches a built-in schemaCurrently only Metaplex Token Metadata: no idl/inline needed. Looked up by discriminator the same way.
NoneNo schema resolvedThe instruction still matches and fires the handler, but params is undefined (you can read raw instruction.data / instruction.accounts).

The order above is the resolution order: inline args+accounts win, then the IDL or bundled schema keyed by discriminator, then nothing.

Anchor IDLs

Point a program at a standard Anchor IDL JSON file (relative to config.yaml). Both legacy (no embedded discriminator) and modern (0.30+, with discriminator) IDLs are supported through the same path. HyperIndex extracts instruction names, argument layouts, ordered account names (including nested account groups), and the IDL types registry.

experimental:
programs:
- name: Jupiter
program_id: JUP6LkbZbjS1jKKwapdHNy74zcZ3tLUZoi5QNyVTaV4
idl: idls/jupiter.json
instructions:
- name: route
discriminator: "0xe517cb977ae3ad2a"
- name: sharedAccountsRoute
discriminator: "0xc1209b3341d69c81"

With an IDL set, list each instruction's name and discriminator; the args and accounts come from the IDL, matched on those discriminator bytes. Don't add inline args/accounts; they're mutually exclusive with idl. Supplying only one of args/accounts is a config error: they must be given together or both omitted.

Inline schema (no IDL)

For programs without an IDL, declare the layout yourself. args is the Borsh argument list (in order, after the discriminator); accounts is the ordered list of account names. They must be provided together.

experimental:
programs:
- name: Raydium
program_id: 675kPX9MHTjS2zt1qfr1NYHuzeLXfQM9H24wFSUt1Mp8
instructions:
- name: swap
discriminator: "0x09"
args:
- { name: amountIn, type: u64 }
- { name: minAmountOut, type: u64 }
accounts: # positional: accounts[0] is tokenProgram, etc.
- tokenProgram
- amm
- userSourceTokenAccount
- userDestTokenAccount
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. Trailing accounts beyond your named list still arrive — see extraAccounts.

Supported argument types

Use these in inline args (and they're what HyperIndex understands from IDLs). The right column is how each value appears in params.args.

typeRendered in params.args as
boolboolean
u8 u16 u32, i8 i16 i32number
u64 u128 i64 i128decimal string (precision-safe)
f32 f64number
stringstring
bytes0x-prefixed hex string
pubkey (alias publicKey)base58 string
{ option: <type> }the value, or null
{ vec: <type> }array
{ array: [<type>, <len>] }array — except [u8, 32], rendered base58 as a pubkey
{ defined: "<TypeName>" }resolved from the IDL's types registry
{ struct: [ {name,type}, … ] }object
{ enum: [ {name, fields?}, … ] }{ VariantName: { …fields } }
args:
- { name: amount, type: u64 }
- { name: authority, type: pubkey }
- { name: maybeOwner, type: { option: pubkey } }
- { name: route, type: { vec: { defined: "RoutePlanStep" } } }
- { name: seedHash, type: { array: [u8, 32] } } # → base58 string

Decoded output

When a schema matches, instruction.params is:

type SvmInstructionParams = {
name: string; // the instruction name
args: unknown; // object keyed by arg name (typed after codegen)
accounts: Record<string, string>; // schema account name -> base58 pubkey
extraAccounts: readonly string[]; // accounts beyond the named list (base58)
};
  • args is keyed by your argument names. After codegen it's typed per instruction; if you bypass that (see below) it's unknown — narrow it with a local type and read defensively.
  • accounts maps each schema account name to its base58 pubkey, e.g. params.accounts.mint. Names come from the IDL or your inline accounts list.
  • extraAccounts collects accounts present on-chain beyond your named list — Anchor remaining_accounts, optional accounts, or accounts resolved from an address lookup table.

Reading args safely

Because args is loosely typed (and u64+ values are strings), read each field as possibly-absent so one missing field can't throw and kill the handler:

import { type SvmInstructionParams } from "envio";

interface JupiterRouteArgs {
inAmount: string;
quotedOutAmount: string;
}

const bi = (x: unknown) =>
x === undefined || x === null ? undefined : BigInt(x as string);

function mapRoute(params: SvmInstructionParams) {
const a = params.args as Partial<JupiterRouteArgs>;
return {
inAmount: bi(a.inAmount),
outAmount: bi(a.quotedOutAmount),
sourceMint: params.accounts.sourceMint,
destMint: params.accounts.destinationMint,
};
}

When params is undefined

Decoding returns undefined (rather than crashing) when:

  • the instruction has no discriminator in config.yaml, so no layout could be looked up;
  • the configured discriminator matched no schema entry (a stale IDL, or the wrong sighash);
  • there were too few accounts for the named list;
  • the argument bytes didn't decode cleanly (wrong/partial layout).

None of these throws, and the indexer keeps running. params is optional in the type system too, so always if (!params) return; before using it, or fall back to the raw instruction.data and instruction.accounts.

Known limitations

  • [u8; 32] is always rendered as a base58 pubkey. A 32-byte hash or Merkle root will look like a pubkey string.
  • Account-count tolerance. Surplus accounts go to extraAccounts; too few means params is undefined.
  • Address lookup tables. ALT-resolved addresses are included in the instruction's account list and mapped positionally — there's no separate ALT handling to configure.
  • Inline nominal types. Reference IDL types via { defined: "Name" }, or declare shapes inline with { struct: … } / { enum: … }. A standalone inline program-level types registry isn't available yet.