Skip to content
helixLaunch

SDK

Build

Build on HELIX

@helixfi/sdk mirrors the program in TypeScript: PDAs, account decoders, an instruction builder for every instruction a client sends (all but the hook-only write_hook_data), quotes that match the program to the lamport, and the events and errors it logs. It is built on @solana/kit, and it is what this site uses.

package.json
// an app inside the HELIX monorepo
{
  "dependencies": {
    "@helixfi/sdk": "workspace:*",
    "@solana/kit": "^8.4.0"
  }
}

Once it is published, it installs with:

shell (after it is on npm)
pnpm add @helixfi/sdk @solana/kit
ModuleExports
constantsHELIX_PROGRAM_ID, EXAMPLE_HOOK_PROGRAM_ID, SYSTEM_PROGRAM_ID, LOADER_UPGRADEABLE_ID, TOKEN_PROGRAM_ID, TOKEN_2022_PROGRAM_ID, ATA_PROGRAM_ID, METAPLEX_PROGRAM_ID, ZERO_ADDRESS, IX, KIND, SIZE, HOOK_FLAGS, HOOK_DISC, RULES, KIT_RULES, MINT_FLAGS, CONTEXT, MODE, LAUNCH_STATE, SIDE, PAUSE, AUTHORITY, BOUNDS, CEILINGS, DEFAULT_CONFIG, BPS, SCALE, LAMPORTS_PER_SOL, MIN_LIQUIDITY, LAUNCH_DECIMALS, HOOK_DATA_LEN
pdaconfigAddress, holdingAddress, poolAddress, launchAddress, positionAddress, hookSignerAddress, hookSelfAddress, extraAccountsAddress, programDataAddress, wrapperAddress, wrappedMintAddress, mirrorAddress, mirrorMintAddress, ataAddress, metaplexMetadataAddress
layoutsdecodeConfig, decodeMint, decodeHolding, decodePool, decodeLaunch, decodePosition, decodeWrapper, decodeMirror, decodeTokenAccount, decodeKitHookData, OFFSETS, and their types
ixcreateMint, createHolding, transfer, mintTo, burn, approve, revoke, setFrozen, closeHolding, setAuthority, setHook, updateMetadata, swap, createPool, addLiquidity, removeLiquidity, collectProtocolFees, createLaunch, claimCreatorFees, claimRewards, shareWithHolders, registerWrapper, wrap, unwrap, createMirror, bridgeOut, bridgeBack, createAtaIdempotent, initConfig, updateConfig, acceptAdmin, exampleHookInit. NO_RULES and the LaunchRules type come from the same module.
mathquoteBuy, quoteSell, curveBuy, curveSell, ammOut, sniperBps, paysHolders, spotPrice, marketCapLamports, curveProgress, sideFeeBps, claimableRewards, bpsOf
hookshookKind, hookAccounts, swapHookAccounts, decodeExtraAccounts, exampleHookExtras, exampleHookConfigAddress
eventsparseEvents, decodeEvent, EVENT, HelixEvent
errorsexplainFailure, errorName, ERRORS
presetsPRESETS
computesetComputeUnitLimit, setComputeUnitPrice, COMPUTE_BUDGET_PROGRAM_ID

Everything is exported from the package root; the builders sit under the ix namespace. Amounts are bigint throughout: lamports, and token base units (6 decimals on launches).

Derive the addresses, decode the pool, quote the buy, and send create_holding + swap. The holding instruction is idempotent, so it can go in front of every buy.

buy.ts
import {
  address,
  appendTransactionMessageInstructions,
  assertIsTransactionWithBlockhashLifetime,
  createSolanaRpc,
  createSolanaRpcSubscriptions,
  createTransactionMessage,
  getBase64Encoder,
  getSignatureFromTransaction,
  pipe,
  sendAndConfirmTransactionFactory,
  setTransactionMessageFeePayerSigner,
  setTransactionMessageLifetimeUsingBlockhash,
  signTransactionMessageWithSigners,
  type Address,
  type Instruction,
  type KeyPairSigner,
} from '@solana/kit';
import {
  configAddress,
  decodeLaunch,
  decodeMint,
  decodePool,
  holdingAddress,
  ix,
  launchAddress,
  poolAddress,
  quoteBuy,
  setComputeUnitLimit,
  swapHookAccounts,
} from '@helixfi/sdk';

const rpc = createSolanaRpc('https://api.devnet.solana.com');
const rpcSubscriptions = createSolanaRpcSubscriptions('wss://api.devnet.solana.com');

/** An account's data, or an error if it doesn't exist. */
async function read(a: Address): Promise<Uint8Array> {
  const { value } = await rpc.getAccountInfo(a, { encoding: 'base64' }).send();
  if (!value) throw new Error('no account at ' + a);
  return new Uint8Array(getBase64Encoder().encode(value.data[0]));
}

/** Signs with every signer the instructions name, sends, waits for confirmation. */
async function send(payer: KeyPairSigner, instructions: Instruction[]) {
  const { value: blockhash } = await rpc.getLatestBlockhash().send();
  const message = pipe(
    createTransactionMessage({ version: 0 }),
    (m) => setTransactionMessageFeePayerSigner(payer, m),
    (m) => setTransactionMessageLifetimeUsingBlockhash(blockhash, m),
    (m) => appendTransactionMessageInstructions(instructions, m),
  );
  const tx = await signTransactionMessageWithSigners(message);
  assertIsTransactionWithBlockhashLifetime(tx);
  await sendAndConfirmTransactionFactory({ rpc, rpcSubscriptions })(tx, { commitment: 'confirmed' });
  return getSignatureFromTransaction(tx);
}

export async function buy(trader: KeyPairSigner, mintAddress: string, lamports: bigint) {
  const mint = address(mintAddress);

  // 1. Addresses: every HELIX account but the mint is a PDA.
  const pool = await poolAddress(mint);
  const launch = await launchAddress(mint);
  const poolHolding = await holdingAddress(mint, pool);
  const traderHolding = await holdingAddress(mint, trader.address);

  // 2. State, decoded at fixed offsets.
  const m = decodeMint(await read(mint));
  const p = decodePool(await read(pool));
  const l = p.launch ? decodeLaunch(await read(launch)) : null;

  // 3. A quote that matches the program to the lamport (before a custom hook's cut).
  const now = BigInt(Math.floor(Date.now() / 1000));
  const q = quoteBuy(p, l, lamports, now, trader.address);
  const minOut = (q.tokensToTrader * 99n) / 100n; // accept 1% slippage

  // 4. Create the holding if needed (idempotent), then swap.
  return send(trader, [
    setComputeUnitLimit(300_000), // a safe ceiling for a trade; simulate to measure what it really uses
    ix.createHolding({ payer: trader, holding: traderHolding, mint, owner: trader.address }),
    ix.swap(
      // A token with a custom hook also needs its extras: swapHookAccounts(m, extras).
      { trader, config: await configAddress(), mint, pool, poolHolding, traderHolding, launch, hook: await swapHookAccounts(m) },
      { side: 'buy', amountIn: lamports, minOut },
    ),
  ]);
}
  • quoteBuy is what the program will do, before a custom hook's cut. q.graduates says whether this buy sells the curve out; then q.solIn is less than offered.
  • A sell is the same call with side: 'sell', amountIn in tokens, and quoteSell(pool, launch, tokens).solOut for the minimum.
  • 300_000 compute units is a safe ceiling for a trade, and 600_000 for a token-to-token route (a sell and a buy in one transaction). They are ceilings, not costs: simulate the transaction to measure what it uses; an external hook adds its own.
  • In a browser wallet, build the same instructions with a signer for the wallet's address and let the wallet sign; see apps/web/src/lib/actions.ts.
read.ts
import { createSolanaRpc, getBase64Encoder, type Address, type Base58EncodedBytes, type Signature } from '@solana/kit';
import { HELIX_PROGRAM_ID, OFFSETS, SIZE, decodeHolding, explainFailure, parseEvents } from '@helixfi/sdk';

const rpc = createSolanaRpc('https://api.devnet.solana.com');

// What a confirmed transaction did, from its logs.
export async function eventsOf(signature: Signature) {
  const tx = await rpc.getTransaction(signature, { encoding: 'json', maxSupportedTransactionVersion: 0, commitment: 'confirmed' }).send();
  const logs = tx?.meta?.logMessages ?? [];
  if (tx?.meta?.err) throw new Error(explainFailure(logs) ?? 'failed');
  return parseEvents(logs); // [{ kind: 'transfer', … }, { kind: 'swap', sol, tokens, creatorFee, … }]
}

// Every HELIX holding of one wallet: filter by size and by the owner's offset.
export async function holdingsOf(owner: Address) {
  const accounts = await rpc
    .getProgramAccounts(HELIX_PROGRAM_ID, {
      encoding: 'base64',
      filters: [
        { dataSize: BigInt(SIZE.holding) },
        { memcmp: { offset: BigInt(OFFSETS.holding.owner), bytes: owner as unknown as Base58EncodedBytes, encoding: 'base58' } },
      ],
    })
    .send();
  return accounts.map((a) => decodeHolding(new Uint8Array(getBase64Encoder().encode(a.account.data[0]))));
}

OFFSETS gives the byte offsets the decoders use for getProgramAccounts filters: a holding's mint and owner, a pool's mint, a launch's mint and creator, a position's pool and owner. The program id is HLX49qz3CSKQrHrdzrrWygZBaKHWWSG8JwUMFHcZ47Fr.

The protocol behind them is on Hooks and Launches; bridging from code is on Bridge. The SDK's tests, packages/sdk/test/*.ts, run every flow on LiteSVM and are the most complete examples.

Paste this into your coding agent's context before asking it to build on HELIX. Every name in it is a real export of the SDK.

prompt for your coding agent
You are writing code against HELIX: a Solana token standard, DEX, launchpad and bridge in ONE program.
Program id: HLX49qz3CSKQrHrdzrrWygZBaKHWWSG8JwUMFHcZ47Fr. SDK: @helixfi/sdk (TypeScript, built on @solana/kit 8).
HELIX tokens are NOT SPL tokens: to hold, move or trade them, never use the Token or Token-2022 programs or associated
token accounts. The one exception is the bridge (below): its instructions move SPL / Token-2022 tokens in and out of HELIX.
Amounts are bigint: lamports (1 SOL = 1_000_000_000n) and token base units (launches use 6 decimals).

ACCOUNTS (owned by the HELIX program; all PDAs except a keypair mint; decode with the SDK)
- Mint: decodeMint. A keypair account when made by createMint or createLaunch (it must sign); a PDA when bridged in,
  wrappedMintAddress(splMint). hookProgram: null = no hook, HELIX_PROGRAM_ID = built-in kit, else external.
- Holding: holdingAddress(mint, owner) = ["holding", mint, owner]; decodeHolding. Exactly one per (mint, owner).
- Pool: poolAddress(mint) = ["pool", mint]; decodePool. Quoted in native SOL. mode: MODE.CURVE or MODE.AMM.
- Launch: launchAddress(mint); decodeLaunch. Position: positionAddress(pool, owner). Config: configAddress(); decodeConfig.

INSTRUCTIONS: the `ix` namespace returns @solana/kit Instructions; signer fields take TransactionSigners.
- ix.createHolding({ payer, holding, mint, owner }): idempotent; prepend it before a buy or an incoming transfer.
- ix.swap({ trader, config, mint, pool, poolHolding, traderHolding, launch, hook }, { side: 'buy' | 'sell', amountIn, minOut })
  buy: amountIn = most lamports to spend, minOut = fewest tokens to receive. sell: amountIn = tokens, minOut = fewest lamports.
  poolHolding = holdingAddress(mint, pool); launch = launchAddress(mint) (any address if the pool has no launch).
- ix.transfer({ authority, source, destination, mint, hook }, amount); ix.mintTo; ix.burn; ix.approve; ix.revoke; ix.closeHolding.
- ix.createLaunch({ creator, config, mint, pool, poolHolding, launch, feeCollector, hook? },
    { name, symbol, uri, rules, customHook?, customHookFlags? })
  mint = await generateKeyPairSigner() (it must sign); feeCollector = decodeConfig(configData).feeCollector.
- ix.claimCreatorFees({ creator, launch }); ix.claimRewards({ owner, mint, launch, holding }); ix.shareWithHolders({ payer, launch }, lamports).
- ix.createPool, ix.addLiquidity, ix.removeLiquidity, ix.collectProtocolFees.

HOOK ACCOUNTS (the `hook` field)
- swaps: await swapHookAccounts(decodedMint, extras). Empty for no hook and for the kit.
- transfers, mints, burns, liquidity: await hookAccounts(mint, decodedMint, extras). [launch] for the kit.
- an external hook's extras: decodeExtraAccounts(data at extraAccountsAddress(hookProgram, mint)), if the hook publishes them.

QUOTES, STATE, EVENTS
- quoteBuy(pool, launch | null, lamportsIn, nowUnixSecs, trader?) -> { solIn, fees, net, tokensToTrader, burned, graduates, impactBps }
- quoteSell(pool, launch | null, tokensIn) -> { gross, fees, burned, solOut, impactBps }. Neither includes an external hook's cut.
- spotPrice, marketCapLamports, curveProgress, sideFeeBps, sniperBps, paysHolders, claimableRewards(launch, holding, now).
- parseEvents(logs) -> HelixEvent[] (kind: 'swap', 'transfer', 'launchCreated', 'graduated', ...). explainFailure(logs) names errors (6000+).

LAUNCH RULES (LaunchRules, integers; start from NO_RULES or PRESETS[i].rules; bounds are BOUNDS)
- creatorFeeBps <= 200; holderFeeBuyBps, holderFeeSellBps <= 300; burnBuyBps, burnSellBps <= 200
- creator + holder + burn <= 500 on each side; maxWalletBps 0 or 50..500; creatorLockSecs <= 31536000
- earlyWindowSecs <= 300 and earlyLockSecs <= 2592000, both set or both 0
- a customHook can't be combined with holder fees, maxWalletBps, creatorLockSecs or the early lock.

BRIDGE (one for one, no bridge fee; the SPL side uses TOKEN_PROGRAM_ID or TOKEN_2022_PROGRAM_ID and ataAddress)
- In, SPL / Token-2022 -> HELIX: wrapper = wrapperAddress(splMint), helixMint = wrappedMintAddress(splMint),
  vault = ataAddress(wrapper, splMint, tokenProgram), ownerToken = ataAddress(owner, splMint, tokenProgram),
  ownerHolding = holdingAddress(helixMint, owner).
  ix.registerWrapper({ payer, splMint, tokenProgram, wrapper, helixMint, vault, metaplexMetadata }) once per SPL mint
  (metaplexMetadata = await metaplexMetadataAddress(splMint)); the name and symbol are read from the token's metadata.
  ix.wrap({ owner, splMint, tokenProgram, wrapper, helixMint, vault, ownerToken, ownerHolding }, amount) after ix.createHolding;
  ix.unwrap(same accounts, amount) after ix.createAtaIdempotent({ payer, ata: ownerToken, owner, mint: splMint, tokenProgram }).
- Out, HELIX -> SPL mirror (a Token-2022 mint, a PDA of HELIX): mirror = mirrorAddress(mint), mirrorMint = mirrorMintAddress(mint),
  vault = holdingAddress(mint, mirror), ownerToken = ataAddress(owner, mirrorMint, TOKEN_2022_PROGRAM_ID).
  ix.createMirror({ payer, helixMint, mirror, mirrorMint, vault, launch }) once per token; a launch only after it graduated
  (launch = launchAddress(mint), any address if the token wasn't launched).
  ix.bridgeOut({ owner, helixMint, mirror, mirrorMint, vault, ownerHolding, ownerToken, hook }, amount) after ix.createAtaIdempotent;
  ix.bridgeBack(same accounts, amount) after ix.createHolding. hook = await hookAccounts(mint, decodedMint, extras);
  the token's hook sees these transfers with context Bridge.
- decodeWrapper, decodeMirror, decodeTokenAccount. A wrapped token can't be mirrored: unwrap it instead.

TRANSACTIONS: prepend setComputeUnitLimit: 300_000 for a trade, 600_000 for a token -> token route (a sell and a buy in one
transaction), 400_000 for a launch with a first buy, a wrap or a bridge out. These are safe ceilings, not costs: simulate the
transaction to measure what it uses (an external hook adds its own). Sign with signTransactionMessageWithSigners so a new
mint's keypair signs too.

HOOKS IN RUST: depend on the helix-hook crate (a path dependency for now). Dispatch on helix_hook::disc. Check that account 0
is a signer equal to PDA(["hook-authority", your program id]) derived from the HELIX program id. TokenHookArgs::decode the
data after the 8-byte discriminator; read args.context (Plain, Buy, Sell, Liquidity, or Bridge: into or out of the
bridge's vault). Answer a before_* callback with HookReturn::encode + set_return_data: at most 3 deltas, each to a writable
HELIX holding of the same mint at account index >= 5.
Never hand-encode HELIX instructions or account layouts: use the SDK.