The Across API is being upgraded. V4 will be deprecated soon and integration support for V4 has ended. To learn more, reach out here.

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. recipient ≠ depositor 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