nktkas/hyperliquid SDK

repository·main·Indexed 19 days ago

https://github.com/nktkas/hyperliquid

A community-supported TypeScript SDK for the Hyperliquid API providing typed interfaces for market data, trading, and real-time updates. It includes specialized clients: InfoClient for read-only market data, ExchangeClient for trading and account management, SubscriptionClient for WebSocket updates, and ExplorerClient for blockchain data. Supports Node.js 22.12+, Bun 1.3.3+, Deno 1.23+, and React Native 0.86+.

Tokens
51.8K
Snippets
169
Records
185
Agent score
63%

What's inside @nktkas/hyperliquid

  1. Overview of Hyperliquid API Clients

    main

    The SDK provides specialized clients for different parts of the Hyperliquid API. Each client uses a transport to communicate with the server.

    APICoverageClient
    InfoMarket data, account stateInfoClient
    ExchangeTrading, fund management, account configurationExchangeClient
    SubscriptionReal-time updates via WebSocketSubscriptionClient
    ExplorerBlocks, transactions, and address activityExplorerClient
  2. Understand SDK versioning and breaking changes

    main

    The SDK follows Semantic Versioning.

    Versioning Rules:

    • Minor version bumps: Breaking changes in the SDK logic.
    • Patch version bumps: Everything else, including changes to request, response, and event types that mirror the Hyperliquid API.

    Important Note on API Types: Because the Hyperliquid API is unversioned and always serves its latest shape, changes to the API's data structures (request/response/event types) are released as patch updates in the SDK, even if they are technically breaking for your code. This is because the break originates from the Hyperliquid API itself.

  3. How Hyperliquid signing works

    main

    Hyperliquid uses two distinct signing flows depending on the action type. Understanding which one to use is critical for custom integrations:

    1. L1 Actions

    Used for trading and position management. These actions are not signed directly. Instead, a "phantom agent" is constructed by hashing the action with a nonce, vault marker, and optional expiration. This hash (connectionId) is then signed using an EIP-712 Agent type.

    • EIP-712 Domain: { name: "Exchange", version: "1", chainId: 1337, verifyingContract: 0x0...0 }
    • Message Type: Agent { source: string, connectionId: bytes32 }
    • Source: "a" for mainnet, "b" for testnet.

    2. User-signed Actions

    Used for fund movements and account security. These actions sign the action fields directly into an EIP-712 message without an intermediate hash.

    • EIP-712 Domain: { name: "HyperliquidSignTransaction", version: "1", chainId: <signatureChainId>, verifyingContract: 0x0...0 }
    • Chain ID: The signatureChainId is determined by the hex value provided within the action itself.

    Shared Requirements

    • Envelope: Both flows must be sent to the exchange endpoint as a JSON object: { action, signature: { r, s, v }, nonce }.
    • Signature Format: ECDSA { r, s, v } where v is 27 or 28.
    • Nonce: A Unix millisecond timestamp. It must be larger than the smallest of the 100 highest nonces stored by Hyperliquid and must fall within (T - 2 days, T + 1 day) of the block timestamp.
    • Hex Case-Sensitivity: All hex values (addresses, multi-sig fields) must be lowercase. The SDK lowercases its own generated values, but any hex you provide manually in an action must be lowercase or the signature will fail.
  4. Import individual methods directly instead of using a client

    main

    To achieve maximum tree-shaking, you can import individual methods directly from their respective API entry points. This approach pulls in only the specific method, its validation schema, and the necessary transport logic, rather than the entire client object.

    Each method accepts a configuration object as its first argument, which follows the pattern of its corresponding client type.

    // Example: Direct method import for an Info method
    import { HttpTransport } from "@nktkas/hyperliquid";
    import { allMids } from "@nktkas/hyperliquid/api/info";
    
    const transport = new HttpTransport();
    const result = await allMids({ transport });
  5. Understand the Hyperliquid error class hierarchy

    main

    The SDK uses typed exceptions that extend HyperliquidError. You can use instanceof checks to distinguish between SDK-thrown errors and external errors.

    Hierarchy Overview:

    • HyperliquidError (Base class)
      • ValidationError: Schema parsing failures (before network I/O).
      • FormatError: Failures in formatPrice or formatSize (before network I/O).
      • AbstractWalletError: Failures in the signing layer (viem, ethers, etc.).
      • CanonicalizeError: Failures in the canonicalize() helper during low-level signing.
      • ApiRequestError: The Hyperliquid API returned an error response.
      • TransportError: Failures at the transport layer.
        • HttpRequestError: fetch failed or returned non-2xx/non-JSON.
        • WebSocketRequestError: WebSocket operation failed.
    import { HyperliquidError } from "@nktkas/hyperliquid";
    
    try {
      // ... sdk call
    } catch (error) {
      if (error instanceof HyperliquidError) {
        // This is an error thrown by the SDK
      } else {
        // This is an error from something else
        throw error;
      }
    }
  6. Choose between HttpTransport and WebSocketTransport

    main

    Every client in the SDK reaches Hyperliquid through a transport. You can choose between two built-in transports:

    • HttpTransport: Each request is an independent POST. It is simpler and ideal for serverless functions, edge workers, or unstable networks where you do not need live data streams.
    • WebSocketTransport: Opens a single persistent connection. It reduces latency by reusing the connection and is the only transport that supports the subscription API for live data.

    Switching between them is typically a one-line change in your client initialization.

    import { HttpTransport, WebSocketTransport } from "@nktkas/hyperliquid";
    
    // Use HttpTransport for standard requests
    const transport = new HttpTransport();
    
    // Use WebSocketTransport if you need subscriptions
    const transport = new WebSocketTransport();
    //                    ^^^^^^^^^^^^^^^^^^^^^^^^^
  7. Use direct method imports for Info, Exchange, Subscription, and Explorer APIs

    main

    When using direct method imports, ensure you pass the correct configuration object as the first argument based on the API type:

    Info methods

    Use InfoClient configuration (e.g., { transport }).

    Exchange methods

    Use ExchangeClient configuration (e.g., { transport, wallet }).

    Subscription methods

    Use SubscriptionClient configuration (e.g., { transport }) and provide a callback function as the second argument.

    Explorer methods

    Use ExplorerClient configuration (e.g., { transport }).

    // Info method example
    import { HttpTransport } from "@nktkas/hyperliquid";
    import { allMids } from "@nktkas/hyperliquid/api/info";
    const transport = new HttpTransport();
    const result = await allMids({ transport });
    
    // Exchange method example
    import { HttpTransport } from "@nktkas/hyperliquid";
    import { order } from "@nktkas/hyperliquid/api/exchange";
    import { privateKeyToAccount } from "viem/accounts";
    const transport = new HttpTransport();
    const wallet = privateKeyToAccount("0x...");
    await order(
      { transport, wallet },
      {
        orders: [{
          a: 0,
          b: true,
          p: "50000",
          s: "0.01",
          r: false,
          t: { limit: { tif: "Gtc" } },
        }],
        grouping: "na",
      },
    );
    
    // Subscription method example
    import { WebSocketTransport } from "@nktkas/hyperliquid";
    import { allMids } from "@nktkas/hyperliquid/api/subscription";
    const transport = new WebSocketTransport();
    const subscription = await allMids({ transport }, (data) => {
      console.log(data.mids);
    });
    
    // Explorer method example
    import { HttpTransport } from "@nktkas/hyperliquid";
    import { blockDetails } from "@nktkas/hyperliquid/api/explorer";
    const transport = new HttpTransport();
    const block = await blockDetails({ transport }, { height: 123 });
  8. Install @nktkas/hyperliquid

    main

    Install the SDK using your preferred package manager based on your runtime.

    Node.js 22.12+ or React Native 0.86+:

    npm i @nktkas/hyperliquid

    Bun 1.3.3+:

    bun add @nktkas/hyperliquid

    Deno 1.23+:

    deno add jsr:@nktkas/hyperliquid
    npm i @nktkas/hyperliquid
  9. Use an agent wallet to avoid repeated signature popups

    main

    By default, every exchange action (trading and position management) triggers a browser wallet popup. Because L1 actions show a non-human-readable hash, it is recommended to use an agent wallet workflow:

    1. Approve the agent: Use the ExchangeClient.approveAgent method once with your browser wallet to authorize an agent address. This will trigger a single wallet popup.
    2. Trade with the agent: Create a new ExchangeClient instance using the agent's private key (e.g., via viem/accounts) as the wallet. Subsequent trades using this agentClient will not trigger browser popups.

    Note: You must persist the agent's private key to reuse it across sessions.

    import { ExchangeClient, HttpTransport } from "@nktkas/hyperliquid";
    import { createWalletClient, custom } from "viem";
    import { generatePrivateKey, privateKeyToAccount } from "viem/accounts";
    import { arbitrum } from "viem/chains";
    
    // Browser wallet (MetaMask, etc.)
    const [account] = await window.ethereum!.request({ method: "eth_requestAccounts" }) as `0x${string}`[];
    const wallet = createWalletClient({ account, chain: arbitrum, transport: custom(window.ethereum!) });
    
    const transport = new HttpTransport();
    const client = new ExchangeClient({ transport, wallet });
    
    // Agent — persist the key to reuse the agent
    const agentPrivateKey = generatePrivateKey();
    const agentSigner = privateKeyToAccount(agentPrivateKey);
    
    // 1. Approve agent once (triggers browser wallet popup)
    await client.approveAgent({
      agentAddress: agentSigner.address,
      agentName: "browser-agent",
    });
    
    // 2. Trade with agent (no popups)
    const agentClient = new ExchangeClient({ transport, wallet: agentSigner });
    await agentClient.order({ orders: [/* ... */], grouping: "na" });
  10. Simulate market orders using IoC limit orders

    main

    Hyperliquid does not provide a native market order type. To simulate a market order, you must use a limit order with the Time-In-Force (TIF) setting tif: "Ioc" (Immediate or Cancel).

    To ensure the order fills immediately at the best available price, you must set an aggressive price relative to the current mid price:

    • For Buys: Set the price above the current mid (e.g., mid * 1.01).
    • For Sells: Set the price below the current mid (e.g., mid * 0.99).

    Note that in volatile markets or for very large orders, a 1% buffer may need to be increased to guarantee execution.

    import { ExchangeClient, HttpTransport, InfoClient } from "@nktkas/hyperliquid";
    import { formatPrice, formatSize, SymbolConverter } from "@nktkas/hyperliquid/utils";
    import { privateKeyToAccount } from "viem/accounts";
    
    const wallet = privateKeyToAccount("0x...");
    
    const transport = new HttpTransport();
    const converter = await SymbolConverter.create({ transport });
    const info = new InfoClient({ transport });
    const exchange = new ExchangeClient({ transport, wallet });
    
    // Parameters
    const coin = "ETH";
    const size = "0.1";
    const isBuy = true;
    const tolerance = 0.01; // 1% price buffer
    
    // Get aggressive price based on current mid price with tolerance
    const mids = await info.allMids();
    const mid = parseFloat(mids[coin]);
    const price = mid * (1 + (isBuy ? tolerance : -tolerance));
    
    // `!` asserts the symbol exists — in production, handle `undefined` explicitly
    const assetId = converter.getAssetId(coin)!;
    const szDecimals = converter.getSzDecimals(coin)!;
    
    // Place IoC order with aggressive price to ensure fill
    await exchange.order({
      orders: [{
        a: assetId,
        b: isBuy,
        p: formatPrice(price, szDecimals),
        s: formatSize(size, szDecimals),
        r: false,
        t: { limit: { tif: "Ioc" } },
      }],
      grouping: "na",
    });
  11. Configure polyfills for React Native

    main

    React Native environments may require polyfills to support the SDK's features. Ensure all polyfills are imported at the very top of your entry file (e.g., index.js) before importing @nktkas/hyperliquid.

    For React Native 0.86+

    If using fastAssetCtxs subscriptions, you must provide DecompressionStream, Web Streams, and TextDecoder (which are missing in Hermes):

    npm i text-encoding-polyfill web-streams-polyfill compression-streams-polyfill
    import "text-encoding-polyfill";
    import "web-streams-polyfill/polyfill";
    import "compression-streams-polyfill";

    For React Native < 0.86

    If Event or EventTarget are missing, polyfill them:

    npm i event-target-shim
    import { Event, EventTarget } from "event-target-shim";
    if (!globalThis.EventTarget) globalThis.EventTarget = EventTarget;
    if (!globalThis.Event) globalThis.Event = Event;

    For React Native < 0.84

    If the native URL is incomplete, add the URL polyfill:

    npm i react-native-url-polyfill
    import "react-native-url-polyfill/auto";
  12. How to use canonicalize to prepare actions for signing

    main

    The signing functions in this SDK do not automatically reorder keys. Because the action hash depends on the specific key order defined in the action's schema, you must ensure your action object matches that order before signing.

    Use canonicalize to reorder an action's keys to match its schema. This function takes the action object and the schema entries (retrieved from the exported Request objects) to guarantee correct serialization.

    Request schemas are exported from @nktkas/hyperliquid/api/exchange using the convention PascalCase(actionType) + "Request" (e.g., CancelRequest for cancel). Use the .entries.action property of the Request object as the schema argument.

    import { canonicalize } from "@nktkas/hyperliquid/signing";
    import { CancelRequest } from "@nktkas/hyperliquid/api/exchange";
    
    const action = canonicalize(CancelRequest.entries.action, {
      cancels: [{ o: 12345, a: 0 }],
      type: "cancel",
    });
    // action is now reordered to: { type: "cancel", cancels: [{ a: 0, o: 12345 }] }
    // This object is now safe to pass to signL1Action or createL1ActionHash