Hyperliquid Withdrawals

Withdraw from Hyperliquid (HyperCore) to any Across-supported chain via a gasless multi-signature flow.

HyperCore is Hyperliquid's margin and matching engine. To move funds off it, the user signs EIP-712 typed signatures that Across submits on-chain. Across handles the HyperCore → HyperEVM lift and delivery to the destination chain.

API key with swap-gasless permission required. Get an API key + Integrator ID.

Endpoints in order of integration

MethodPathPurpose
GET/swap/gaslessQuote — returns the steps the user must sign
POST/gasless/submitSubmit signed steps; relayer takes over from here
GET/deposit/status?depositId=...&originChainId=1337Track the withdrawal to terminal status

Supported Hyperliquid Origin Tokens

SymbolinputToken valueDecimalsBalance lives in
USDC-spot0x20000000000000000000000000000000000000008HL spot
USDC-perps0x21000000000000000000000000000000000000008HL perps margin
HYPE0x22222222222222222222222222222222222222228HL spot
USDT (USDT-SPOT)0x200000000000000000000000000000000000010C8HL spot

The 0x2… addresses are HyperCore sentinels — they are not HyperEVM ERC-20s. Destinations: any EVM chain Across supports.

End-to-end integration flow

Quote

GET /swap/gasless
  ?inputToken=0x2100000000000000000000000000000000000000   # USDC-PERPS
  &originChainId=1337                                       # HyperCore
  &outputToken=0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913   # USDC on Base
  &destinationChainId=8453                                  # Base
  &amount=100000000                                         # 1 USDC (8 decimals on HC)
  &depositor=<user EOA>
  &recipient=<destination addr>
  &integratorId=<your id>

Decimals trap. amount is in the input token's decimals. HyperCore tokens use 8 decimals (HyperEVM and standard EVM USDC use 6) — passing a 6-decimal value for an 8-decimal HyperCore input under-bridges by 100×.

Returns depositId, the steps to sign in swapTxns[], quoteExpiryTimestamp, expectedOutputAmount, minOutputAmount, and fees (the submission fee is at fees.submission — there is no top-level submissionFees).

Submission fee — the amount (in the input token) paid to the submitter that broadcasts your signed permit on-chain in the gasless flow, reimbursing their gas plus margin.

It's part of the signed witness (submissionFees: { amount, recipient }), so the user authorizes the exact fee and payee when signing — no separate gas payment from the user.

See the API reference for the full response shape.

Sign every step

Iterate swapTxns and dispatch by step.ecosystem. The user signs every step — missing signatures are rejected at submit. Please note that even if a single signature is unsigned or signed incorrectly, the transaction will not go through.

const sigs: Record<string, string> = {};
for (const step of quote.swapTxns) {
  if (step.ecosystem === 'hypercore') {
    sigs[step.stepId] = await hlSign(step.typedData);   // HL wallet
  } else if (step.ecosystem === 'evm-gasless') {
    sigs[step.stepId] = await evmSign(step.typedData); // EVM EOA — EIP-712
  }
}

evm-gasless chainId gotcha. Sign at step.typedData.domain.chainId (always 999 / HyperEVM), NOT the outer step.chainId. If you use a single EVM wallet for both ecosystems, make sure it is on chain 999 before signing every step. Wrong-chain signatures fail server-side verifyTypedData.

Submit

POST /gasless/submit
{
  "swapTx":             <quote.swapTx>,
  "swapTxns":           <quote.swapTxns>,
  "signaturesByStepId": { "hypercore-transfer": "0x…", "gasless-auth": "0x…" }
}

Returns { depositId, messageId, id } (messageId may be null). Submit is gated in two stages: (1) every signature must recover to the depositor (else 400 "Invalid signature: unable to verify signature"); (2) the integrator key must be eligible for gasless submission (else 403). It is accepted optimistically — a 200 is returned before on-chain execution, so an underfunded withdrawal still returns 200 and later surfaces as deposit-failed.

See the API reference for the full response shape.

Track status

Poll /deposit/status with the depositId and originChainId=1337:

GET /deposit/status?depositId=<id>&originChainId=1337
StatusMeaning
deposit-pendingHyperCore → HyperEVM lift in progress
deposit-failedLift failed (e.g. insufficient HyperCore balance); funds not moved (terminal)
filledDestination tokens delivered (terminal)
expiredDestination tokens were not delivered within the fillDeadline and the user is now eligible for a refund
refundedTerminal state; user has received a refund

See the API reference for the full response shape.

Refunds land on HyperEVM, not HyperCore. refundToken.chainId on bridging quotes is 999. Set refundOnOrigin: true and surface the destination in your UI — users won't expect refunds to arrive on HyperEVM.

Gas Fees

HyperCore → HyperEVM mirror moves cost a small HyperEVM gas fee.

  • USDC / USDT origin — fee is paid in-kind or auto-deducted in HYPE. Users don't need to think about it.
  • HYPE / non-stablecoin origin — users must hold HYPE on HyperCore to cover gas.

This lift cost is reported at fees.submission on every quote (a flat amount in the submission token, independent of size). fees.total / fees.totalMax cover any protocol/relayer spread — 0 for same-asset USDC routes.

Minimal Example: HC USDC-PERPS → Base ETH

const quote = await fetch(`${BASE}/swap/gasless?inputToken=0x2100…&…`).then(r => r.json());

const sigs: Record<string, string> = {};
for (const step of quote.swapTxns) {
  sigs[step.stepId] = step.ecosystem === 'hypercore'
    ? await hlSign(step.typedData)
    : await evmSign(step.typedData);
}

const { depositId } = await fetch(`${BASE}/gasless/submit`, {
  method: 'POST',
  body: JSON.stringify({
    swapTx: quote.swapTx,
    swapTxns: quote.swapTxns,
    signaturesByStepId: sigs,
  }),
}).then(r => r.json());

while (true) {
  const { status } = await fetch(
    `${BASE}/deposit/status?depositId=${depositId}&originChainId=1337`,
  ).then(r => r.json());
  if (['filled', 'expired', 'refunded', 'deposit-failed'].includes(status)) break;
  await new Promise(r => setTimeout(r, 2000));
}

Caveats

  1. HL nonces are wallet-unique. If your client races two quotes, sign + submit them in order — HL will reject a later nonce that arrives first as stale.
  2. Quote expiry is short (typically 60–120s). After quoteExpiryTimestamp, re-quote; submitting a stale quote returns a witness-binding error.
  3. Don't mutate swapTx or swapTxns between quote and submit. The submit endpoint re-validates byte-for-byte; any drift returns a deep-equal error.
  4. Trust fees.submission.amount from the quote rather than computing the lift fee yourself.
  5. Very large amounts may exceed a route's available liquidity and return 400 AMOUNT_TOO_HIGH (the max is in the message).
  6. recipientdepositor is fine — but the depositor EOA must own the HC funds and sign every step.

All validation errors return 400 InvalidParamError with a param field telling you which input to fix. Most "re-quote" errors are TOCTOU freshness checks — retrying the same body will keep failing.

References

On this page