Sign and Broadcast Transaction

To complete any staking or withdrawal operation on Hyperliquid, sign the unsigned transaction provided by the Staking API and broadcast it to the network.

1. Prepare the transaction

Retrieve an unsignedTransaction object from the relevant API endpoint, e.g., /staking/transfer, /staking/delegate, /staking/undelegate, /staking/withdraw.

2. Sign the transaction

Sign the unsigned transaction using your local signer or the following code:

import { signUserSignedAction, userSignedActionEip712Types, isValidPrivateKey } from "@nktkas/hyperliquid/signing";

async function signTransaction(privateKey, unsignedTransaction) {
  const action = unsignedTransaction.action;

  return Buffer.from(
    JSON.stringify({
      ...unsignedTransaction,
      signature: await signUserSignedAction({
        wallet: privateKey,
        action: action,
        types: userSignedActionEip712Types[action.type],
      }),
    })
  ).toString("base64");
}

(async () => {
  const [, , rawTransaction, privateKey] = process.argv;

  if (!rawTransaction || typeof rawTransaction !== "string") {
    console.error("Invalid raw transaction. It should be a base64-encoded string.");
    process.exit(1);
  }

  if (!privateKey || typeof privateKey !== "string" || !isValidPrivateKey(privateKey)) {
    console.error("Invalid private key.");
    process.exit(1);
  }

  const unsignedTransaction = JSON.parse(Buffer.from(rawTransaction, "base64").toString("utf-8"));

  if (
    !("action" in unsignedTransaction) ||
    !("type" in unsignedTransaction.action) ||
    !["cDeposit", "tokenDelegate", "cWithdraw"].includes(unsignedTransaction.action.type)
  ) {
    console.error("Invalid unsigned transaction.");
    process.exit(1);
  }

  console.log(await signTransaction(privateKey.startsWith("0x") ? privateKey : `0x${privateKey}`, unsignedTransaction));
})();

/**
 *  To run with env variables:
 *
 *  $ export RAW_TX= 'rawtxhere'
 *  $ export PRIVATE_KEY= 'privatekeyhere'
 *  $ npx --yes -p @nktkas/[email protected] node hyperliquid-signer.js $RAW_TX $PRIVATE_KEY
 **/

Upon successful execution, the script prints the signed transaction, ready for broadcasting.

3. Send the transaction

Broadcast the signed transaction to the Hyperliquid network by sending a POST request to /api/v1/hyperliquid/{network}/transaction/send.

Example request (for testnet network):

curl --request POST \
     --url https://api-test.p2p.org/api/v1/hyperliquid/testnet/transaction/send \
     --header 'accept: application/json' \
     --header 'authorization: Bearer <token>' \
     --header 'content-type: application/json' \
     --data '
{
  "signedTransaction": "string"
}
'
  • signedTransaction — signed transaction in the hexadecimal format which needs to be broadcasted to the network.

Example response:

{
  "result": {
    "createdAt": "2025-10-10T05:28:10.434Z",
    "network": "mainnet"
  },
  "error": {}
}
  • createdAt — timestamp of the transaction in the ISO 8601 format.
  • network — network name.

What's next?