Skip to content
Alchemy Logo

Hyperliquid Transactions Quickstart

By the end of this tutorial, you will have a React application that uses Privy for embedded wallet authentication and @alchemy/wallet-apis to send sponsored HyperEVM transactions.

Install Wallet APIs, Privy, and viem:

npm install @alchemy/wallet-apis @privy-io/react-auth viem

You will also need:

  • An Alchemy API key for an app with Hyperliquid enabled.
  • A Gas Manager policy ID for sponsoring gas.
  • A Privy App ID with embedded Ethereum wallets enabled.

Make sure Hyperliquid is enabled as a network on your Alchemy app and that your gas policy is linked to the same app.

Hyperliquid dashboard support

Wrap your app with PrivyProvider and configure Privy to create an embedded Ethereum wallet when a user logs in.

App.tsx
import { PrivyProvider } from "@privy-io/react-auth";
 
export function App() {
  return (
    <PrivyProvider
      appId={process.env.NEXT_PUBLIC_PRIVY_APP_ID!}
      config={{
        embeddedWallets: {
          ethereum: {
            createOnLogin: "all-users",
          },
          showWalletUIs: false,
        },
      }}
    >
      <YourApp />
    </PrivyProvider>
  );
}

Use Privy's toViemAccount helper to convert the embedded wallet into a viem-compatible signer. Wallet APIs uses this signer to sign the prepared smart account operations.

usePrivySigner.ts
import { toViemAccount, useWallets } from "@privy-io/react-auth";
import { useEffect, useState } from "react";
import type { LocalAccount } from "viem";
 
export function usePrivySigner() {
  const {
    wallets: [wallet],
  } = useWallets();
 
  const [signer, setSigner] = useState<LocalAccount>();
 
  useEffect(() => {
    if (!wallet || signer) return;
    toViemAccount({ wallet }).then(setSigner);
  }, [wallet, signer]);
 
  return signer;
}

viem exports the HyperEVM chain as hyperEvm. Pass it to createSmartWalletClient with your Privy signer and Alchemy transport.

walletClient.ts
import {
  alchemyWalletTransport,
  createSmartWalletClient,
} from "@alchemy/wallet-apis";
import type { LocalAccount } from "viem";
import { hyperEvm } from "viem/chains";
 
export function createHyperliquidWalletClient(signer: LocalAccount) {
  return createSmartWalletClient({
    signer,
    transport: alchemyWalletTransport({
      apiKey: process.env.NEXT_PUBLIC_ALCHEMY_API_KEY!,
    }),
    chain: hyperEvm,
    paymaster: {
      policyId: process.env.NEXT_PUBLIC_ALCHEMY_GAS_POLICY_ID!,
    },
  });
}

To test on Hyperliquid testnet, import hyperliquidEvmTestnet from viem/chains instead and configure the matching network and gas policy in the Alchemy Dashboard.

HyperEVM currently uses non-7702 mode with Wallet APIs. Request a smart account first, then pass the returned smart account address to sendCalls.

requestAccount returns a stable address for the signer and request parameters. The example below stores it in React state, so each send can pass the same address to sendCalls without requesting the account again.

SendTransaction.tsx
import { useCallback, useEffect, useMemo, useState } from "react";
import { zeroAddress } from "viem";
import type { Address, LocalAccount } from "viem";
import { createHyperliquidWalletClient } from "./walletClient";
 
export function SendTransaction({ signer }: { signer: LocalAccount }) {
  const client = useMemo(
    () => createHyperliquidWalletClient(signer),
    [signer],
  );
  const [accountAddress, setAccountAddress] = useState<Address>();
  const [status, setStatus] = useState<string>();
 
  useEffect(() => {
    setAccountAddress(undefined);
    client
      .requestAccount({
        creationHint: { accountType: "sma-b" },
      })
      .then(({ address }) => setAccountAddress(address));
  }, [client]);
 
  const sendTransaction = useCallback(async () => {
    if (!accountAddress) return;
    setStatus("sending");
    const { id } = await client.sendCalls({
      account: accountAddress,
      calls: [
        {
          to: zeroAddress,
          value: 0n,
          data: "0x",
        },
      ],
    });
 
    setStatus("waiting");
 
    const result = await client.waitForCallsStatus({ id });
    setStatus(result.status);
  }, [accountAddress, client]);
 
  const isPending = status === "sending" || status === "waiting";
 
  return (
    <button onClick={sendTransaction} disabled={!accountAddress || isPending}>
      {!accountAddress
        ? "Loading account..."
        : status === "sending"
          ? "Sending..."
          : status === "waiting"
            ? "Waiting..."
            : "Send transaction"}
    </button>
  );
}

Render the button after the user logs in and Privy has created the signer:

Wallet.tsx
import { usePrivy } from "@privy-io/react-auth";
import { SendTransaction } from "./SendTransaction";
import { usePrivySigner } from "./usePrivySigner";
 
export function Wallet() {
  const { ready, authenticated, login, logout } = usePrivy();
  const signer = usePrivySigner();
 
  if (!ready) return <p>Loading...</p>;
 
  if (!authenticated) {
    return <button onClick={() => login()}>Login with Privy</button>;
  }
 
  return (
    <div>
      <button onClick={() => logout()}>Logout</button>
      {signer ? <SendTransaction signer={signer} /> : <p>Loading signer...</p>}
    </div>
  );
}

You can also sponsor HyperEVM contract calls that interact with HyperCore. The example below encodes a sendRawAction call to the HyperCore Writer contract.

This encodes a real HyperCore order action. Use testnet or replace the order parameters before sending from a production wallet.

hyperliquidOrder.ts
import {
  encodeAbiParameters,
  encodeFunctionData,
  hexToBytes,
  toHex,
} from "viem";
 
export const CORE_WRITER_ADDRESS =
  "0x3333333333333333333333333333333333333333" as const;
 
export const HYPERLIQUID_CALLDATA = (() => {
  const asset = 0; // BTC
  const isBuy = true;
  const limitPx = 100000000000n; // $100,000
  const sz = 100000n; // 0.001 BTC
  const reduceOnly = false;
  const encodedTif = 2; // GTC
  const cloid = 0n;
 
  const payloadHex = encodeAbiParameters(
    [
      { type: "uint32" },
      { type: "bool" },
      { type: "uint64" },
      { type: "uint64" },
      { type: "bool" },
      { type: "uint8" },
      { type: "uint128" },
    ],
    [asset, isBuy, limitPx, sz, reduceOnly, encodedTif, cloid],
  );
 
  // Encoding version (1 byte) + action ID (3 bytes).
  const prefix = new Uint8Array([0x01, 0x00, 0x00, 0x01]);
 
  const payload = hexToBytes(payloadHex);
  const actionBytes = new Uint8Array(prefix.length + payload.length);
  actionBytes.set(prefix, 0);
  actionBytes.set(payload, prefix.length);
 
  const coreWriterAbi = [
    {
      type: "function",
      name: "sendRawAction",
      stateMutability: "nonpayable",
      inputs: [{ name: "data", type: "bytes", internalType: "bytes" }],
      outputs: [],
    },
  ] as const;
 
  return encodeFunctionData({
    abi: coreWriterAbi,
    functionName: "sendRawAction",
    args: [toHex(actionBytes)],
  });
})();

Then reuse the stored smart account address and pass the encoded call to sendCalls:

sendHyperliquidOrder.ts
import { createHyperliquidWalletClient } from "./walletClient";
import {
  CORE_WRITER_ADDRESS,
  HYPERLIQUID_CALLDATA,
} from "./hyperliquidOrder";
import type { Address, LocalAccount } from "viem";
 
export async function sendHyperliquidOrder({
  signer,
  accountAddress,
}: {
  signer: LocalAccount;
  accountAddress: Address;
}) {
  const client = createHyperliquidWalletClient(signer);
 
  const { id } = await client.sendCalls({
    account: accountAddress,
    calls: [
      {
        to: CORE_WRITER_ADDRESS,
        data: HYPERLIQUID_CALLDATA,
        value: 0n,
      },
    ],
  });
 
  const result = await client.waitForCallsStatus({ id });
  console.log("Hyperliquid order status:", result.status);
 
  return result;
}

Was this page helpful?