Hyperliquid Withdrawals
Withdraw from Hyperliquid (HyperCore) to any Across-supported chain via a gasless multi-signature flow.
HyperCore is Hyperliquid's margin and state matching engine. To move funds off it the user signs user signs EIP-712 typed signatures, which is submitted onchain by Across's relayers. Across handles the HyperCore → HyperEVM lift, the bridge, and any destination swap.
API key with swap-gasless permission required. Get an API key + Integrator ID.
Endpoints
| 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 |
One flow, one response shape
Every withdrawal uses the same flow and the same quote shape. What differs per route is (a) the steps in swapTxns[] and (b) the settlement mechanism, reported in steps.bridge.provider:
steps.bridge.provider | When | Steps emitted | Notes |
|---|---|---|---|
sponsored-cctp | USDC → another chain | hypercore-transfer + gasless-auth | USDC always settles via Circle CCTP (sponsored). Scales to large sizes. |
across | non-USDC / non-CCTP bridges | hypercore-transfer + gasless-auth | Across intent/relayer network. Liquidity-capped — oversized amounts return 400 AMOUNT_TOO_HIGH. |
hypercore-passthrough | same-token → HyperEVM (999) | hypercore-transfer | No relayer leg. |
hypercore-usdclass-transfer | same-chain SPOT ↔ PERPS | usdclass-transfer | HyperCore-internal move. |
Before You Integrate — gotchas that will trip you up, read these first:
- Decimals trap. HyperCore USDC = 8 decimals. HyperEVM USDC and standard EVM USDC = 6 decimals. The
amountquery parameter is in input-token decimals — passing a 6-decimal value when the input lives on HyperCore will under-bridge by 100×. - Refunds land on HyperEVM, not HyperCore.
refundToken.chainIdon bridging quotes is always999. SetrefundOnOrigin: trueon bridging routes and surface the destination explicitly in your UI — users won't expect refunds to come back to HyperEVM.
Supported 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
Quote
GET /swap/gasless
?inputToken=0x2100000000000000000000000000000000000000 # USDC-PERPS
&originChainId=1337 # HyperCore
&outputToken=0x0000000000000000000000000000000000000000 # native ETH
&destinationChainId=8453 # Base
&amount=100000000 # 1 USDC (8 decimals on HC)
&depositor=<user EOA>
&recipient=<destination addr>
&integratorId=<your id>Returns depositId, swapTxns[], swapTx (echoed periphery payload — {} on single-step routes), quoteExpiryTimestamp, expectedOutputAmount, minOutputAmount, and fees (the submission fee is at fees.submission — there is no top-level submissionFees). Also approvalTxns (always null), and informational checks / steps — steps.bridge.provider is the settlement mechanism; steps.originSwap / steps.destinationSwap are null when there's no swap leg. swapTxns is always present.
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.
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.
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 / refunded | Terminal failure (refunds land 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.
Caveats
- Decimals trap. HC tokens = 8 decimals; standard EVM USDC = 6. Re-check before passing
amount. - 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. - USDC always settles via CCTP (
steps.bridge.provider: sponsored-cctp) — it is not a separate flow. Always trustfees.submission.amountfrom the quote rather than computing the lift fee yourself. across(Intents) routes are liquidity-capped. Oversized amounts return400 AMOUNT_TOO_HIGHwith the max in the message. CCTP (USDC) routes are not capped and scale to large sizes.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.
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));
}Good to Know
- API keys — you need a key with the
swap-gaslesspermission. Get one via the integrator form.
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