Skip to main content

Integration Guide

HyperAMM is Helm's core liquidity-pool contract.

This guide shows the minimum contract flow for integrating HyperAMM. Examples use Viem-style calls and assume you already loaded the current ABIs and addresses.

warning

Do not copy contract addresses from examples. Use the current address registry and verify chain ID, contract address, ABI, and paused state before every production integration.

Tools and Inputs

Tooling

The hyperamm-js repository contains two packages for integrators:

  • hyperamm-sdk (@helmfinancex/hyperamm-sdk) packages HyperAMM contract addresses, ABIs, and TypeScript helpers for swaps, LP deposits, LP withdrawals, and pool reads.
  • hyperamm-cli (@helmfinancex/hyperamm-cli) provides command-line tooling backed by the SDK for scripting and operational workflows.

Use this guide as the protocol-level reference for validating what the library does under the hood: previews, approvals, slippage floors, execution fees, and post-submit tracking still matter for production integrations.

You need:

  • Hyperliquid EVM RPC
  • Current HyperAMM contract addresses
  • Current ABIs for the contracts you call
  • A wallet client for write actions
  • A public client for reads and simulations
  • Token decimals for every input and output token

For user-facing writes, use this order:

  1. Discover contracts and pool state.
  2. Read preview values and operation fees from RPC.
  3. Compute a nonzero slippage floor.
  4. Check ERC-20 allowances and prompt for approval if needed.
  5. Re-read the final preview if approval, bridge, or market state may have changed.
  6. Simulate the exact main transaction with the final parameters and native value.
  7. Prompt the wallet for the main transaction.
  8. Verify events and final state.

Pool Discovery

Use the Factory as the canonical registry for per-pool components.

discover-pool.ts
const poolInfo = await publicClient.readContract({
address: factoryAddress,
abi: hyperAMMFactoryAbi,
functionName: "getPoolInfo",
args: [poolId],
});

const {
hyperAMM,
pool,
oracle,
hyperCoreManager,
hyperCoreTrader,
rebalancer,
executor,
withdrawer,
escrow,
reconciler,
compositionManager,
swapFeeModule,
stakingManager,
sHYPE,
} = poolInfo.deployment;

For pair-based discovery, use getPoolByPair(token0, token1, isToken0Based) and confirm the returned address is not zero.

Swap

Swaps are exact-input and execute through HyperAMMSwapRouter.

Input Specification

swap and previewLiquidityQuote both take a DirectSwapParams object.

FieldTypeDescription
poolsaddress[]Ordered list of HyperAMM pool addresses used for the route. Each pool must be registered in the router.
isZeroToOnesbool[]Direction for each hop. true swaps pool token0 to token1; false swaps pool token1 to token0. Length must match pools.
amountInsuint256[]Input amount for each hop. Use 0 to consume the full available balance for that hop. Length must match pools.
isTokenOutHypeboolIf true and tokenOut is WHYPE, the router unwraps WHYPE and sends native HYPE to recipient.
tokenInaddressInitial input token. For native HYPE input, set this to WHYPE and send msg.value = amountIn.
tokenOutaddressFinal output token. For native HYPE output, set this to WHYPE and set isTokenOutHype = true.
recipientaddressOutput recipient. If zero address, output is sent to msg.sender.
amountInuint256Exact input amount for the full route, in tokenIn decimals. Must be greater than zero.
amountOutMinuint256Minimum acceptable final output. Applies only to the final route output, not intermediate hops.
deadlineuint256Unix timestamp. Must be current or future block time and no more than five minutes ahead.

Route rules:

  • pools, isZeroToOnes, and amountIns must have the same length.
  • The route must have at least one hop and cannot exceed the router's maxHops.
  • The final hop output token must match tokenOut.
  • Intermediate balances must be fully consumed by later hops. A route that leaves unused intermediate tokens in the router reverts.
  • For a simple single-hop swap, amountIns can be [amountIn] or [0].
  • For multi-hop routes, use 0 on intermediate hops when the next hop should consume all output from the previous hop.
swap-preview.ts
const params = {
pools: [poolAddress],
isZeroToOnes: [true],
amountIns: [amountIn],
isTokenOutHype: false,
tokenIn,
tokenOut,
recipient: account.address,
amountIn,
amountOutMin: 0n,
deadline,
};

const [amountOut] = await publicClient.readContract({
address: swapRouterAddress,
abi: hyperAMMSwapRouterAbi,
functionName: "previewLiquidityQuote",
args: [params],
});

const amountOutMin = applySlippage(amountOut, slippageBps);

Then simulate and execute with the same route.

swap-execute.ts
const finalParams = { ...params, amountOutMin };
const nativeInput = tokenIn.toLowerCase() === whypeAddress.toLowerCase() && useNativeHype;

const { request } = await publicClient.simulateContract({
account,
address: swapRouterAddress,
abi: hyperAMMSwapRouterAbi,
functionName: "swap",
args: [finalParams],
value: nativeInput ? amountIn : 0n,
});

const hash = await walletClient.writeContract(request);

Rules:

  • ERC-20 input requires approval to HyperAMMSwapRouter.
  • Native HYPE input uses tokenIn = WHYPE and msg.value = amountIn.
  • Native HYPE output uses tokenOut = WHYPE and isTokenOutHype = true.
  • Use amountOutMin = 0 only for preview reads. The executable swap transaction must use a nonzero amountOutMin.

LP Deposit

LP deposits are submitted to the per-pool HyperAMMExecutor. The submit transaction creates an operation; automated execution mints the LP shares later.

deposit-preview.ts
const expectedShares = await publicClient.readContract({
address: lensAddress,
abi: hyperAMMLensAbi,
functionName: "previewDeposit",
args: [hyperAMMAddress, amount],
});

const executionFee = await publicClient.readContract({
address: executorAddress,
abi: hyperAMMExecutorAbi,
functionName: "executionFee",
});

const minShares = applySlippage(expectedShares, slippageBps);
deposit-submit.ts
const nativeDeposit = baseToken.toLowerCase() === whypeAddress.toLowerCase() && useNativeHype;

const { request } = await publicClient.simulateContract({
account,
address: executorAddress,
abi: hyperAMMExecutorAbi,
functionName: "submitDeposit",
args: [amount, minShares, deadline],
value: executionFee + (nativeDeposit ? amount : 0n),
});

const hash = await walletClient.writeContract(request);

Rules:

  • ERC-20 deposits require approval to the per-pool HyperAMMExecutor.
  • Native HYPE deposits into WHYPE-based pools use msg.value = amount + executionFee.
  • The submit transaction is not final settlement. Track the operation until it is executed or refunded.

LP Withdraw

There are two LP withdrawal paths.

PathUse whenContract call
Instant withdrawEVM-side liquidity is available and the user accepts the feesubmitWithdraw or submitWithdrawNative
Queued withdrawDefault exit path with queue and later claimsubmitQueuedWithdraw or submitQueuedWithdrawNative

Preview queued withdraws through the Lens.

queued-withdraw-preview.ts
const expectedAmount = await publicClient.readContract({
address: lensAddress,
abi: hyperAMMLensAbi,
functionName: "previewQueuedWithdraw",
args: [hyperAMMAddress, shares],
});

const minAmount = applySlippage(expectedAmount, slippageBps);

Submit the request through the Executor.

queued-withdraw-submit.ts
const executionFee = await publicClient.readContract({
address: executorAddress,
abi: hyperAMMExecutorAbi,
functionName: "executionFee",
});

const { request } = await publicClient.simulateContract({
account,
address: executorAddress,
abi: hyperAMMExecutorAbi,
functionName: useNativeHype ? "submitQueuedWithdrawNative" : "submitQueuedWithdraw",
args: [shares, minAmount, deadline],
value: executionFee,
});

const hash = await walletClient.writeContract(request);

submitQueuedWithdraw returns an executor operation ID. It does not return the withdrawer requestId. The withdrawer request is created later when the executor processes the operation. Track the executor OperationSubmitted and QueuedWithdrawExecuted events, or read the user's request IDs from HyperAMMWithdrawer, before claiming.

queued-withdraw-request-id.ts
const requestIds = await publicClient.readContract({
address: withdrawerAddress,
abi: hyperAMMWithdrawerAbi,
functionName: "getUserWithdrawRequests",
args: [account.address],
});

const requestId = requestIds.at(-1);
if (requestId === undefined) throw new Error("queued withdrawal request not found");

After the withdrawal request exists, claim from HyperAMMWithdrawer only when it is claimable.

claim-queued-withdraw.ts
const claimable = await publicClient.readContract({
address: withdrawerAddress,
abi: hyperAMMWithdrawerAbi,
functionName: "isClaimable",
args: [requestId],
});

if (!claimable) throw new Error("withdrawal is not claimable");

const { request } = await publicClient.simulateContract({
account,
address: withdrawerAddress,
abi: hyperAMMWithdrawerAbi,
functionName: "claim",
args: [requestId],
});

const hash = await walletClient.writeContract(request);

Rules:

  • LP shares are ERC-20 tokens. Withdraw submit calls require LP token approval.
  • Queued withdrawal amounts are determined at request time.
  • A queued withdrawal may require a later claim transaction.
  • If native HYPE delivery fails, check the per-pool Escrow.

LP Value

Use HyperAMM.lpValue() to read total pool value for LP display. The value is denominated in the pool's base token: token0 when isToken0Based is true, otherwise token1.

lp-value.ts
const [
totalLpValue,
totalSupply,
userShares,
isToken0Based,
token0,
token1,
token0Decimals,
token1Decimals,
] = await publicClient.multicall({
contracts: [
{
address: hyperAMMAddress,
abi: hyperAMMAbi,
functionName: "lpValue",
},
{
address: hyperAMMAddress,
abi: erc20Abi,
functionName: "totalSupply",
},
{
address: hyperAMMAddress,
abi: erc20Abi,
functionName: "balanceOf",
args: [account.address],
},
{
address: hyperAMMAddress,
abi: hyperAMMAbi,
functionName: "isToken0Based",
},
{
address: hyperAMMAddress,
abi: hyperAMMAbi,
functionName: "token0",
},
{
address: hyperAMMAddress,
abi: hyperAMMAbi,
functionName: "token1",
},
{
address: hyperAMMAddress,
abi: hyperAMMAbi,
functionName: "token0Decimals",
},
{
address: hyperAMMAddress,
abi: hyperAMMAbi,
functionName: "token1Decimals",
},
],
allowFailure: false,
});

if (totalLpValue <= 0n || totalSupply === 0n) {
throw new Error("LP value is unavailable");
}

const baseToken = isToken0Based ? token0 : token1;
const baseDecimals = isToken0Based ? token0Decimals : token1Decimals;
const userLpValue = (userShares * totalLpValue) / totalSupply;

Use computeLpValue(maximise) only when you need the same directional valuation used by pool operations. Deposits use maximised pricing; withdrawals use conservative pricing. For executable deposit or withdrawal estimates, prefer the Lens preview methods shown above.

Next Steps