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

Solana Configuration File

A Solana indexer is defined by a config.yaml with ecosystem: svm. It tells HyperIndex which chain to read, which programs and instructions to match, and how to decode them. This page is the field-by-field reference; for the meaning of discriminators, IDLs and argument types see Decoding & IDLs.

Add this line at the top of the file for editor autocompletion and validation:

# yaml-language-server: $schema=./node_modules/envio/svm.schema.json

A complete example

config.yaml
# yaml-language-server: $schema=./node_modules/envio/svm.schema.json
name: my-solana-indexer
description: Index Jupiter swaps and Metaplex NFT mints
ecosystem: svm
chains:
- start_block: 437000000 # a SLOT number, not a block
experimental:
hypersync_config:
url: https://solana.hypersync.xyz # required: the HyperSync endpoint serving instructions
programs:
# --- decoded from an Anchor IDL ---
- name: Jupiter
program_id: JUP6LkbZbjS1jKKwapdHNy74zcZ3tLUZoi5QNyVTaV4
idl: idls/jupiter.json
instructions:
- name: sharedAccountsRoute
discriminator: "0xc1209b3341d69c81"
field_selection:
transaction_fields: [signature, feePayer, success]
token_balance_fields: true
# --- decoded from an inline schema (no IDL) ---
- name: Raydium
program_id: 675kPX9MHTjS2zt1qfr1NYHuzeLXfQM9H24wFSUt1Mp8
instructions:
- name: swap
discriminator: "0x09"
args:
- { name: amountIn, type: u64 }
- { name: minAmountOut, type: u64 }
accounts:
- tokenProgram
- amm
- userSourceTokenAccount
- userDestTokenAccount
field_selection:
token_balance_fields: true
transaction_fields: [signature]

Top-level fields

FieldRequiredDefaultNotes
name-Project name.
ecosystem-Must be svm.
chains-One or more chains to index (see below).
description--Free-text description.
schema-schema.graphqlPath to your GraphQL schema.
handlers-src/handlersDirectory that handler files are auto-loaded from.
full_batch_size-5000Batch size for processing.
storage-postgres: trueStorage backends (postgres, clickhouse).
disable_default_cross_chain-falseMake entities and effect caches per-chain instead of shared across chains.
No EVM-style global fields

For Solana, several EVM top-level fields don't apply: contracts, rollback_on_reorg, save_full_history, raw_events, a global field_selection, and address_format. Reorgs are handled automatically on the HyperSync source (it rolls back on reorg); the RPC source indexes finalized data only. Field selection is per-instruction only (see field selection).

chains

Each entry is one Solana cluster.

FieldRequiredDefaultNotes
start_block-The slot to start indexing from.
experimental--HyperSync-backed instruction indexing: hypersync_config + programs (see below). The key is named experimental to signal that this shape is still evolving.
rpc--RPC URL. Required only when experimental is not set; ignored in favour of the HyperSync source when it is.
end_block--Stop at this slot (inclusive of the range processed). Useful for finite backfills and tests.
block_lag--Stay this many slots behind the head.
skip-falseSkip this chain.
EVM difference: no chain id

EVM chains require an id (the public chain ID). Solana chains have no id - the cluster is identified by the endpoints you point at. Inside handlers the Solana chain id is 0.

experimental

Everything HyperSync-backed lives under the chain's experimental key:

FieldRequiredDefaultNotes
hypersync_config-Block containing url. Both the block and url are required: a chain with an experimental key but no hypersync_config fails config validation rather than falling back to a default.
programs-Solana programs to index on this chain (see below).

Choosing an endpoint and a start slot

There's one Solana HyperSync endpoint per network:

NetworkEndpoint
Mainnethttps://solana.hypersync.xyz
Devnethttps://solana-devnet.hypersync.xyz

Each endpoint serves data back to its own history floor, and that floor moves forward over time. Mainnet currently starts around slot 403,000,000 (roughly six months of history). Query GET <endpoint>/height for the current head, and probe the floor (below) rather than hard-coding a number into a config as if it were permanent. Need data further back? Tell us on Discord.

A start_block below the floor doesn't error

The server serves from the floor instead. An indexer with no end_block looks like it's syncing fine but silently skips every slot before the floor; an indexer whose end_block is also below the floor gets an empty page back with next_slot unchanged, so it never makes progress at all.

To check whether a candidate slot is above or below the current floor, send a one-slot bounded query and compare next_slot to what you asked for: equal means below the floor, from_slot + 1 means served. The same technique works against either endpoint; shown here against mainnet.

curl -sS -X POST https://solana.hypersync.xyz/query \
-H "Authorization: Bearer $ENVIO_API_TOKEN" \
-H 'Content-Type: application/json' \
-d '{"from_slot": 403000000, "to_slot": 403000001,
"include_all_blocks": true,
"field_selection": {"block": ["slot"]}}' | jq .next_slot
# equal to from_slot => below the floor
# from_slot + 1 => served

The to_slot is what makes this a reliable probe; without it the server skips ahead to the floor and answers successfully from any from_slot. This checks one candidate; if the floor has moved past it, repeat with a higher slot (or binary-search between it and the current head from GET <endpoint>/height) until you find the lowest slot that's served. Pick the network you need, then set start_block at or above its floor.

programs

FieldRequiredDefaultNotes
name-A unique name you choose. Used in handlers (onInstruction({ program: "<name>" })) and generated types.
program_id-The base58 program address.
instructions-Instructions to match within this program (see below).
idl--Path to an Anchor IDL JSON, relative to config.yaml. If set, HyperIndex derives each instruction's args + accounts from the IDL entry matching that instruction's configured discriminator. Mutually exclusive with per-instruction inline args/accounts.
handler-autoPath to the file that registers this program's handlers. By default handler files are auto-loaded.

instructions

Each entry selects one instruction of the program to index. Only name is required by the schema, but in practice discriminator is required too: it is both how HyperIndex tells your instruction apart from the program's others and how it resolves the decode layout.

FieldRequiredDefaultNotes
name-The instruction name, unique per program. It is the key in onInstruction({ instruction: "<name>" }) and in the generated types. It is a label only: matching is done by discriminator, so the name need not equal the IDL's.
discriminator--Hex bytes that identify the instruction (e.g. "0xc1209b3341d69c81"). Always set it, including for modern Anchor IDLs. HyperIndex reads the discriminator only from this key, never from an IDL's embedded discriminator array, and it is also the key it looks the IDL layout up by. Omit it and the instruction matches every instruction of the program and decodes nothing (params is undefined). See discriminators.
is_inner-unsettrue = inner (CPI) only, false = top-level only, omitted = matches both.
args--Inline argument schema (Borsh), { name, type } per arg. Requires accounts too. Mutually exclusive with the program's idl. See supported types.
accounts--Inline ordered list of account names. The Nth name labels the Nth account. Requires args too.
account_filters--Restrict matches by the pubkey in specific account positions (see below).
field_selection--Opt into extra data on the event (see below).

Field selection

By default a handler receives only the instruction itself, plus its block's slot/time/hash. Opt into more data per instruction:

KeyValueAdds to the handler's instruction
transaction_fieldslist of field namesinstruction.transaction.<field> for each selected field: signature, allSignatures, feePayer, success, err, fee, computeUnitsConsumed, accountKeys, recentBlockhash, version, transactionIndex.
block_fieldslist of field namesinstruction.block.<field> on top of the always-present slot/time/hash: height, parentSlot, parentHash.
token_balance_fieldstrueinstruction.transaction.tokenBalances: pre/post SPL Token balances. Independent of transaction_fields.
log_fieldstrueinstruction.logs: program logs scoped to this instruction.
field_selection:
transaction_fields: [signature, feePayer, success]
block_fields: [height]
token_balance_fields: true
log_fields: true
signature vs allSignatures

signature is the scalar transaction id (a string) and is what nearly every handler wants. allSignatures is the full readonly string[] of signatures from every signer, which only matters for multi-signer analysis. They are selected independently: selecting signature does not give you allSignatures.

Unselected fields are typed as compile errors in the handler (FieldNotSelected), so reading a field you forgot to select fails at tsc time, not silently at runtime.

Account filters

Match an instruction only when specific account positions hold specific pubkeys. This is useful to index, say, only swaps that touch a particular pool or mint. Positions are 05; within a position values are OR-ed, and across positions they are AND-ed.

instructions:
- name: swap
discriminator: "0x09"
account_filters:
- position: 1
values:
- 58oQChx4yWmvKdwLLZzBi4ChoCc2fqCUWBkwMihLYQo2 # only this pool

Use any_of to OR several AND-groups together:

    account_filters:
any_of:
- [ { position: 0, values: [So11111111111111111111111111111111111111112] } ]
- [ { position: 1, values: [EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v] } ]

Choosing what to index

Solana's highest-frequency programs (SPL Token, System) produce enormous volumes of instructions. Matching them directly can swamp a backfill. Two practical patterns:

  • Index DeFi/protocol instructions and read value flow from token balances. Enabling token_balance_fields on a protocol instruction gives you the transaction's net token movements without indexing every Transfer.
  • Scope high-volume instructions with account_filters or a tight slot window (start_block/end_block) when you do need them.