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
| Method | Path | Purpose |
|---|---|---|
GET | /swap/gasless | Quote — returns the steps the user must sign |
POST | /gasless/submit | Submit signed steps; relayer takes over from here |
GET | /deposit/status?depositId=...&originChainId=1337 | Track the withdrawal to terminal status |
Supported Hyperliquid Origin Tokens
| Symbol | inputToken value | Decimals | Balance lives in |
|---|---|---|---|
USDC-spot | 0x2000000000000000000000000000000000000000 | 8 | HL spot |
USDC-perps | 0x2100000000000000000000000000000000000000 | 8 | HL perps margin |
HYPE | 0x2222222222222222222222222222222222222222 | 8 | HL spot |
USDT (USDT-SPOT) | 0x200000000000000000000000000000000000010C | 8 | HL 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| Status | Meaning |
|---|---|
deposit-pending | HyperCore → HyperEVM lift in progress |
deposit-failed | Lift failed (e.g. insufficient HyperCore balance); funds not moved (terminal) |
filled | Destination tokens delivered (terminal) |
expired | Destination tokens were not delivered within the fillDeadline and the user is now eligible for a refund |
refunded | Terminal 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
- 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.
- Quote expiry is short (typically 60–120s). After
quoteExpiryTimestamp, re-quote; submitting a stale quote returns a witness-binding error. - Don't mutate
swapTxorswapTxnsbetween quote and submit. The submit endpoint re-validates byte-for-byte; any drift returns a deep-equal error. - Trust
fees.submission.amountfrom the quote rather than computing the lift fee yourself. - Very large amounts may exceed a route's available liquidity and return
400 AMOUNT_TOO_HIGH(the max is in the message). recipient≠depositoris fine — but thedepositorEOA 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
- GET /swap/gasless — quote endpoint reference
- POST /gasless/submit — submit endpoint reference
- Working with Hypercore — companion deposits guide
- Tracking Deposits — generic deposit polling for non-gasless flows
- Refunds — refund mechanics on Across