> ## Documentation Index
> Fetch the complete documentation index at: https://docs.magic.link/llms.txt
> Use this file to discover all available pages before exploring further.

# Hyperliquid Trading

> Trade perpetual futures on Hyperliquid using Magic Express Server Wallets with TEE-backed signing

## Overview

This guide shows how to use **Magic's Express Server Wallet** to trade perpetual futures on [Hyperliquid](https://hyperliquid.xyz/). The [`@nktkas/hyperliquid`](https://github.com/nktkas/hyperliquid) SDK accepts any viem-compatible account, so your server creates a custom account that delegates `signTypedData` to the TEE — the private key never leaves the enclave.

***

## Prerequisites

Before starting, ensure you have:

1. A Magic **Secret Key** — from your [Magic Dashboard](https://dashboard.magic.link)
2. An **OIDC Provider ID** — configured for your auth provider ([setup guide](/server-wallets/express-api/identity-provider))
3. A user JWT from your authentication provider
4. USDC on Arbitrum One and ETH for gas (to activate the wallet on Hyperliquid mainnet — required even for testnet)

### How It Works

1. Your server authenticates the user and obtains a JWT
2. JWT is sent to Magic's TEE to get (or create) the user's EOA
3. Your app deposits at least \$5 USDC from Arbitrum to activate the wallet on Hyperliquid
4. For testnet: claim 1,000 mock USDC via the faucet API
5. A custom viem account delegates `signTypedData` calls to the TEE
6. The `@nktkas/hyperliquid` ExchangeClient uses this account for order signing
7. Orders, cancellations, and leverage updates are signed and submitted to Hyperliquid

<Info>
  Hyperliquid uses EIP-712 typed data signatures for all exchange actions. The `@nktkas/hyperliquid` SDK handles nonces and signature construction automatically — your TEE account only needs to implement `signTypedData`.
</Info>

***

## TEE Request Helper

All TEE calls use the same authentication headers. Create a reusable helper for your server-side code.

```ts TypeScript icon="square-js" theme={null}
const TEE_BASE = "https://tee.express.magiclabs.com";

async function teeRequest<T>(path: string, jwt: string, body: object): Promise<T> {
  const res = await fetch(`${TEE_BASE}${path}`, {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      Authorization: `Bearer ${jwt}`,
      "X-Magic-Secret-Key": "YOUR_SECRET_KEY",
      "X-OIDC-Provider-ID": "YOUR_OIDC_PROVIDER_ID",
      "X-Magic-Chain": "ETH",
    },
    body: JSON.stringify(body),
  });
  if (!res.ok) throw new Error(`TEE error: ${res.status}`);
  return res.json();
}
```

***

## Get or Create a Wallet

Fetch the user's wallet address. If one doesn't exist, it will be created automatically.

```ts TypeScript icon="square-js" theme={null}
const { public_address: eoaAddress } = await teeRequest<{ public_address: string }>(
  "/v1/wallet", jwt, {}
);
```

***

## Creating a TEE-Backed Account

The Hyperliquid SDK expects a wallet with a `signTypedData` method. Create a custom viem account that delegates signing to the TEE.

```ts TypeScript icon="square-js" theme={null}
import { toAccount } from "viem/accounts";
import { hashTypedData, hashMessage, serializeSignature, type Address, type Hex } from "viem";

function createTeeAccount(jwt: string, address: Address) {
  return toAccount({
    address,
    signMessage: async ({ message }) => {
      const hash = typeof message === "string"
        ? hashMessage(message)
        : hashMessage({ raw: message as Hex });
      return signHashViaTee(jwt, hash);
    },
    signTransaction: async () => {
      throw new Error("Use signAndSend for transactions");
    },
    signTypedData: async ({ domain, types, primaryType, message }) => {
      const hash = hashTypedData({ domain, types, primaryType, message });
      return signHashViaTee(jwt, hash);
    },
  });
}

async function signHashViaTee(jwt: string, hash: Hex): Promise<Hex> {
  const { r, s, v } = await teeRequest<{ r: string; s: string; v: string }>(
    "/v1/wallet/sign/data", jwt, { raw_data_hash: hash, chain: "ETH" }
  );
  return serializeSignature({ r: r as Hex, s: s as Hex, yParity: Number(v) - 27 });
}

const teeAccount = createTeeAccount(jwt, eoaAddress as Address);
```

***

## Setting Up Hyperliquid Clients

Install dependencies and initialize the SDK with the TEE-backed account.

```bash theme={null}
npm install @nktkas/hyperliquid viem
```

```ts TypeScript icon="square-js" theme={null}
import { ExchangeClient, InfoClient, HttpTransport } from "@nktkas/hyperliquid";

// Testnet transport
const transport = new HttpTransport({ isTestnet: true });

// Read-only client for market data (no wallet needed)
const info = new InfoClient({ transport });

// Trading client (uses TEE-backed account for signing)
const exchange = new ExchangeClient({ transport, wallet: teeAccount });
```

***

## Depositing USDC

Hyperliquid runs on its own L1 chain — you can't trade directly from an Arbitrum wallet. To fund your Hyperliquid account, send USDC to the bridge contract on Arbitrum. This is a standard ERC20 `transfer` — your Hyperliquid account is credited within \~1 minute.

First, set up a public client and transaction helper for Arbitrum One:

```ts TypeScript icon="square-js" theme={null}
import {
  createPublicClient, http, encodeFunctionData, parseUnits,
  serializeTransaction, keccak256, type TransactionSerializable,
} from "viem";
import { arbitrum } from "viem/chains";

const arbClient = createPublicClient({
  chain: arbitrum,
  transport: http("https://arb1.arbitrum.io/rpc"),
});

async function signAndSend(jwt: string, tx: TransactionSerializable) {
  const serialized = serializeTransaction(tx);
  const hash = keccak256(serialized);

  const { r, s, v } = await teeRequest<{ r: string; s: string; v: string }>(
    "/v1/wallet/sign/data", jwt, { raw_data_hash: hash, chain: "ETH" }
  );

  const yParity = Number(v) >= 27 ? Number(v) - 27 : Number(v);
  const signed = serializeTransaction(tx, { r: r as Hex, s: s as Hex, yParity });
  return await arbClient.sendRawTransaction({ serializedTransaction: signed });
}
```

Then deposit USDC to the bridge:

```ts TypeScript icon="square-js" theme={null}
const BRIDGE_ADDRESS = "0x2Df1c51E09aECF9cacB7bc98cB1742757f163dF7";
const USDC_ADDRESS = "0xaf88d065e77c8cC2239327C5EDb3A432268e5831";

const usdcAbi = [
  {
    name: "transfer",
    type: "function",
    inputs: [
      { name: "to", type: "address" },
      { name: "amount", type: "uint256" },
    ],
    outputs: [{ type: "bool" }],
    stateMutability: "nonpayable",
  },
] as const;

// Deposit 10 USDC into Hyperliquid
const amount = parseUnits("10", 6); // USDC has 6 decimals

const nonce = await arbClient.getTransactionCount({ address: eoaAddress as Address });
const txHash = await signAndSend(jwt, {
  to: USDC_ADDRESS as Address,
  data: encodeFunctionData({
    abi: usdcAbi,
    functionName: "transfer",
    args: [BRIDGE_ADDRESS, amount],
  }),
  chainId: arbitrum.id,
  nonce,
});

console.log("Deposit tx:", txHash);
// Funds arrive on Hyperliquid within ~1 minute
```

<Warning>
  The minimum deposit is **5 USDC**. Deposits below this amount are not credited and cannot be recovered.
</Warning>

***

## Testnet Faucet

Once a wallet has been activated with a mainnet deposit of at least \$5 USDC, you can claim 1,000 mock USDC on Hyperliquid testnet. This is a one-time claim per address.

```ts TypeScript icon="square-js" theme={null}
const response = await fetch("https://api.hyperliquid-testnet.xyz/info", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({
    type: "claimDrip",
    user: eoaAddress,
  }),
});

const result = await response.json();
console.log("Faucet claim:", result);
```

After claiming, switch to testnet clients for development and testing.

***

## Withdrawing USDC

To withdraw USDC from Hyperliquid back to Arbitrum, use the SDK's `withdraw3` method. The withdrawal is signed on Hyperliquid and processed by validators — no Arbitrum transaction is needed from the user.

```ts TypeScript icon="square-js" theme={null}
// Withdraw 10 USDC back to Arbitrum
await exchange.withdraw3({
  destination: eoaAddress,
  amount: "10",
});

// Funds arrive on Arbitrum within ~3-4 minutes
```

***

## Reading Market Data

Use the `InfoClient` to fetch prices, order books, and account state. These calls are read-only and don't require signing.

```ts TypeScript icon="square-js" theme={null}
// Get all mid prices
const mids = await info.allMids();
console.log("BTC:", mids["BTC"], "ETH:", mids["ETH"]);

// Get L2 order book for BTC
const book = await info.l2Book({ coin: "BTC", nSigFigs: 3 });
console.log("Best bid:", book.levels[0][0]); // { px, sz, n }
console.log("Best ask:", book.levels[1][0]);

// Get user's positions and margin
const state = await info.clearinghouseState({ user: eoaAddress });
console.log("Account value:", state.marginSummary.accountValue);
console.log("Positions:", state.assetPositions);
```

***

## Placing Orders

The ExchangeClient handles nonce generation and EIP-712 signing automatically.

### Limit Order

Place a Good-Til-Canceled limit order at a specific price.

```ts TypeScript icon="square-js" theme={null}
const result = await exchange.order({
  orders: [{
    a: 0,           // Asset index (0 = BTC)
    b: true,        // Buy (true = long, false = short)
    p: "95000",     // Limit price
    s: "0.01",      // Size in base currency
    r: false,       // Not reduce-only
    t: { limit: { tif: "Gtc" } },
  }],
  grouping: "na",
});

console.log("Order status:", result.response.data.statuses);
// [{ resting: { oid: 12345 } }]
```

### Market Order

Hyperliquid doesn't have a native market order type. Use an Immediate-or-Cancel (IOC) limit order with a slippage price.

```ts TypeScript icon="square-js" theme={null}
// Get current price for slippage calculation
const mids = await info.allMids();
const currentPrice = parseFloat(mids["ETH"]);
const slippagePrice = (currentPrice * 1.01).toFixed(1); // +1% for buys

const result = await exchange.order({
  orders: [{
    a: 4,                    // Asset index (4 = ETH)
    b: true,                 // Buy
    p: slippagePrice,        // Max price willing to pay
    s: "0.1",                // Size
    r: false,
    t: { limit: { tif: "Ioc" } },
  }],
  grouping: "na",
});

console.log("Fill:", result.response.data.statuses);
// [{ filled: { totalSz: "0.1", avgPx: "3456.78" } }]
```

<Info>
  For market buys, set the slippage price **above** the current mid. For market sells, set it **below**. The unfilled portion of an IOC order is automatically canceled.
</Info>

***

## Canceling Orders

```ts TypeScript icon="square-js" theme={null}
// Cancel by order ID
await exchange.cancel({
  cancels: [
    { a: 0, o: 12345 }, // Cancel order 12345 on BTC
  ],
});

// Cancel by client order ID
await exchange.cancelByCloid({
  cancels: [{ asset: 0, cloid: "0x0001" }],
});
```

***

## Updating Leverage

```ts TypeScript icon="square-js" theme={null}
await exchange.updateLeverage({
  asset: 0,         // BTC
  isCross: true,    // Cross-margin mode
  leverage: 10,
});
```

***

## Switching to Production

To move from testnet to mainnet, remove the `isTestnet` flag from the transport. The deposit and withdrawal code already uses mainnet addresses.

```ts TypeScript icon="square-js" theme={null}
// Mainnet transport (default)
const transport = new HttpTransport();

const info = new InfoClient({ transport });
const exchange = new ExchangeClient({ transport, wallet: teeAccount });
```

<Info>
  Asset indices can change between testnet and mainnet. In production, use `info.meta()` to look up the correct index for each asset dynamically rather than hardcoding values.
</Info>

***

## Key Dependencies

| Package                                                        | Purpose                                                    |
| -------------------------------------------------------------- | ---------------------------------------------------------- |
| [`@nktkas/hyperliquid`](https://github.com/nktkas/hyperliquid) | TypeScript SDK for Hyperliquid exchange API                |
| [`viem`](https://viem.sh/)                                     | Ethereum client, account utilities, and typed data hashing |

***

## TEE Endpoints Used

| Endpoint                    | Purpose                                        |
| --------------------------- | ---------------------------------------------- |
| `POST /v1/wallet`           | Get or create an EOA wallet                    |
| `POST /v1/wallet/sign/data` | Sign a raw data hash (EIP-712 typed data hash) |

***

## Troubleshooting

<AccordionGroup>
  <Accordion title="TEE returns 401 or 403">
    **Symptoms:** Authentication errors when calling TEE endpoints.

    **Solutions:**

    * Verify the JWT token is valid and not expired
    * Check that `X-Magic-Secret-Key` matches your dashboard credentials
    * Ensure the OIDC Provider ID is correct
    * Confirm your domain is allowlisted in the Magic Dashboard
  </Accordion>

  <Accordion title="Order rejected with invalid signature">
    **Symptoms:** Hyperliquid rejects the order with a signature error.

    **Solutions:**

    * Verify the `signHashViaTee` function correctly reconstructs the signature from r, s, v components
    * Ensure the TEE account address matches the address that deposited USDC on Hyperliquid
    * Check that `serializeSignature` is imported from `viem`
  </Accordion>

  <Accordion title="Insufficient margin">
    **Symptoms:** Order rejected due to insufficient margin or balance.

    **Solutions:**

    * Deposit USDC to Hyperliquid using the bridge contract (see [Depositing USDC](#depositing-usdc) above)
    * Check available margin with `info.clearinghouseState({ user: address })`
    * Ensure at least 5 USDC was deposited (minimum)
    * Reduce order size or increase leverage
  </Accordion>

  <Accordion title="Asset index not found">
    **Symptoms:** Order rejected because the asset index is invalid.

    **Solutions:**

    * Use `info.meta()` to look up the correct asset index for each coin
    * Asset indices may differ between testnet and mainnet
    * Common indices: 0 = BTC, 4 = ETH (but always verify with `meta()`)
  </Accordion>
</AccordionGroup>

***

## Resources

<CardGroup cols={2}>
  <Card title="Express API Docs" icon="brackets-curly" href="/server-wallets/express-api/overview">
    Learn about Magic's Express Server Wallet API
  </Card>

  <Card title="Hyperliquid Documentation" icon="book" href="https://hyperliquid.gitbook.io/hyperliquid-docs">
    Official Hyperliquid protocol documentation
  </Card>

  <Card title="@nktkas/hyperliquid SDK" icon="github" href="https://github.com/nktkas/hyperliquid">
    TypeScript SDK with full API coverage
  </Card>

  <Card title="Hyperliquid Testnet" icon="globe" href="https://app.hyperliquid-testnet.xyz">
    Testnet UI for deposits and testing
  </Card>
</CardGroup>

***
