Skip to main content
CoreWriter is the bridge that lets a HyperEVM smart contract act on HyperCore. Your contract places orders, moves funds between accounts, stakes, and delegates—the same actions a trader sends to the /exchange endpoint—directly from Solidity. This guide explains how CoreWriter encodes those actions, walks through a contract you can deploy on a Chainstack HyperEVM endpoint, and covers the account prerequisites that decide whether your actions actually land.
Prerequisites

Two directions: read precompiles and CoreWriter

HyperEVM and HyperCore are two execution environments on the same chain, and contracts move data between them in two directions:
  • Read — HyperCore exposes read precompiles starting at 0x0000000000000000000000000000000000000800. A contract calls them to query perp positions, spot balances, oracle prices, staking delegations, and the L1 block number. The returned values match HyperCore state at the time the EVM block is built.
  • Write — the CoreWriter system contract at 0x3333333333333333333333333333333333333333 sends actions to HyperCore. This guide covers the write direction.
This split mirrors the API: read precompiles are the on-chain equivalent of /info queries, and CoreWriter is the on-chain equivalent of signed /exchange actions. The difference is that CoreWriter actions are authored by the calling contract’s own address—the contract is the HyperCore actor, so no off-chain signature is involved.
CoreWriter actions are submitted as ordinary HyperEVM transactions, so deploying the contract and calling CoreWriter both run against your Chainstack HyperEVM (/evm) endpoint—a contract drives HyperCore orders and transfers without the signed /exchange API. Chainstack also serves the /info reads used to verify results (clearinghouseState, spotClearinghouseState, openOrders). The one step that needs Hyperliquid’s public /exchange endpoint is the big-block toggle below. See Hyperliquid methods for the full endpoint matrix.

How CoreWriter works

CoreWriter exposes a single function:
interface ICoreWriter {
    function sendRawAction(bytes calldata data) external;
}
When you call sendRawAction, the contract burns roughly 25,000 gas and emits a log that HyperCore picks up and processes as an action. A basic call costs around 47,000 gas in total. The action runs as the caller’s HyperCore user—the contract address that invoked sendRawAction.

Action encoding

The data argument is a byte string with a fixed 4-byte header followed by an ABI-encoded payload:
BytesMeaning
Byte 1Encoding version. Only version 1 is supported today; the version byte keeps the format upgradeable.
Bytes 2–4Action ID, as a big-endian unsigned integer.
Remaining bytesThe action payload—the raw ABI encoding of a sequence of Solidity types specific to the action.
In Solidity this is exactly abi.encodePacked(uint8(1), uint24(actionId), abi.encode(...fields)): the uint8 is the version byte, the uint24 is the 3-byte big-endian action ID, and abi.encode produces the payload.

Supported actions

CoreWriter supports the following actions. The fields are ABI-encoded in the order shown. Action ID 14 is intentionally absent.
IDActionFieldsSolidity types
1Limit order(asset, isBuy, limitPx, sz, reduceOnly, encodedTif, cloid)(uint32, bool, uint64, uint64, bool, uint8, uint128)
2Vault transfer(vault, isDeposit, usd)(address, bool, uint64)
3Token delegate(validator, wei, isUndelegate)(address, uint64, bool)
4Staking depositweiuint64
5Staking withdrawweiuint64
6Spot send(destination, token, wei)(address, uint64, uint64)
7USD class transfer(ntl, toPerp)(uint64, bool)
8Finalize EVM contract(token, encodedFinalizeEvmContractVariant, createNonce)(uint64, uint8, uint64)
9Add API wallet(apiWalletAddress, apiWalletName)(address, string)
10Cancel order by oid(asset, oid)(uint32, uint64)
11Cancel order by cloid(asset, cloid)(uint32, uint128)
12Approve builder fee(maxFeeRate, builder)(uint64, address)
13Send asset(destination, subAccount, sourceDex, destinationDex, token, wei)(address, address, uint32, uint32, uint64, uint64)
15Borrow lend operation(encodedOperation, token, wei)(uint8, uint64, uint64)
16Set abstraction(user, abstraction)(address, uint8)
A few encoding conventions are easy to get wrong:
  • Limit orderencodedTif is 1 for Alo, 2 for Gtc, 3 for Ioc. cloid of 0 means no client order ID; any other value is used as the cloid. limitPx and sz are sent as 10^8 times the human-readable value, and sz must respect the asset’s szDecimals (see Make sure your action lands).
  • Finalize EVM contractencodedFinalizeEvmContractVariant is 1 for Create, 2 for FirstStorageSlot, 3 for CustomStorageSlot. createNonce is only used with the Create variant.
  • Add API wallet — an empty apiWalletName makes this the main API wallet (agent).
  • Approve builder feemaxFeeRate is in decibps. To approve a 0.01% fee, pass 10.
  • Send asset — if subAccount is not the zero address, the transfer comes from that sub-account. Use uint32 max for sourceDex or destinationDex to mean spot.
  • Borrow lend operationencodedOperation is 0 for Supply, 1 for Withdraw. A wei of 0 applies the operation maximally (for example, withdraw the full reserve balance).
  • Set abstractionabstraction is 1 for disabled (standard), 2 for unifiedAccount, 3 for portfolioMargin. user can be the master user or a sub-account.

Encode an action yourself

The contract below wraps two actions—a USD class transfer (move USDC between the perp and spot wallets) and a limit order—and exposes a generic sendRawAction passthrough for any action you encode yourself. It depends on nothing but the CoreWriter interface.
CoreWriterCaller.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;

interface ICoreWriter {
    function sendRawAction(bytes calldata data) external;
}

contract CoreWriterCaller {
    ICoreWriter constant CORE_WRITER =
        ICoreWriter(0x3333333333333333333333333333333333333333);

    // Action 7: move USDC between this contract's spot and perp balances.
    // ntl is the amount in perp units (USDC has 6 decimals on the perp side).
    function usdClassTransfer(uint64 ntl, bool toPerp) external {
        bytes memory action =
            abi.encodePacked(uint8(1), uint24(7), abi.encode(ntl, toPerp));
        CORE_WRITER.sendRawAction(action);
    }

    // Action 1: place a limit order.
    // limitPx and sz are 10^8 * the human-readable value; sz must respect the
    // asset's szDecimals. encodedTif: 1 = Alo, 2 = Gtc, 3 = Ioc. cloid 0 = none.
    function limitOrder(
        uint32 asset,
        bool isBuy,
        uint64 limitPx,
        uint64 sz,
        bool reduceOnly,
        uint8 encodedTif,
        uint128 cloid
    ) external {
        bytes memory action = abi.encodePacked(
            uint8(1),
            uint24(1),
            abi.encode(asset, isBuy, limitPx, sz, reduceOnly, encodedTif, cloid)
        );
        CORE_WRITER.sendRawAction(action);
    }

    // Escape hatch: send any action by ID with a pre-encoded payload.
    function sendRawAction(uint24 actionId, bytes calldata payload) external {
        CORE_WRITER.sendRawAction(abi.encodePacked(uint8(1), actionId, payload));
    }
}
abi.encodePacked(uint8(1), uint24(actionId), ...) lays the header out exactly as the encoding table describes: the uint8 occupies one byte, the uint24 occupies three big-endian bytes, and the ABI-encoded payload follows. Building the action this way keeps the contract self-contained, so callers pass plain arguments instead of pre-formatted byte strings.

Use hyper-evm-lib in production

Encoding actions by hand is useful for understanding the wire format, but most builders use hyper-evm-lib—an MIT-licensed, actively maintained Solidity library (under the hyperliquid-dev org) that wraps every CoreWriter action and read precompile with typed helpers, handles the EVM↔Core decimal conversions, and ships a Foundry test engine that simulates HyperCore locally so you can test contracts without deploying to testnet. Install it with Foundry:
forge install hyperliquid-dev/hyper-evm-lib
The same two actions, plus an on-chain readiness check, with the library:
TraderVault.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;

import {CoreWriterLib} from "hyper-evm-lib/src/CoreWriterLib.sol";
import {PrecompileLib} from "hyper-evm-lib/src/PrecompileLib.sol";

contract TraderVault {
    using CoreWriterLib for *;

    // Move USDC from spot to perp, then place a GTC limit order.
    // Call only after this contract's HyperCore account exists and holds USDC.
    function openPosition(
        uint32 asset,
        bool isBuy,
        uint64 limitPx,
        uint64 sz,
        uint64 perpUsdc
    ) external {
        CoreWriterLib.transferUsdClass(perpUsdc, true); // spot -> perp
        CoreWriterLib.placeLimitOrder(asset, isBuy, limitPx, sz, false, 2, 0);
    }

    // Confirm on-chain that this contract is a HyperCore user before acting.
    function isReady() external view returns (bool) {
        return PrecompileLib.coreUserExists(address(this));
    }
}
CoreWriterLib exposes named helpers for the full action set—placeLimitOrder, transferUsdClass, spotSend, delegateToken, vaultTransfer, setAbstraction, bridgeToCore, and more—each of which encodes the action exactly as the table above describes. PrecompileLib mirrors the read precompiles, including coreUserExists and perpAssetInfo (which returns szDecimals).

Make sure your action lands

CoreWriter actions are fire-and-forget. sendRawAction emits its log and the EVM transaction succeeds whether or not HyperCore accepts the action. If the action is malformed or the account is not set up, HyperCore drops it silently—no EVM revert, no error event, and the order simply never appears in openOrders. This is the single most common source of confusion, so check the following before concluding that CoreWriter is broken.

The account must exist on HyperCore first

A CoreWriter action runs as the contract’s HyperCore user, and that user must already exist on HyperCore before the EVM block is built. An address becomes a HyperCore user once it receives a Core asset such as USDC.
Initializing the account with a HyperEVM-to-HyperCore transfer in the same block as the action does not work—the action is still rejected. Fund the contract’s HyperCore account in an earlier block, then send actions.
Send a small amount of USDC to the contract’s address on HyperCore in a separate, earlier transaction. You can confirm the account exists on-chain by calling the core-user-exists precompile at 0x0000000000000000000000000000000000000810 (PrecompileLib.coreUserExists(address) in hyper-evm-lib) before sending the action.

Perp orders need USDC on the perp side

Bridging USDC from HyperEVM lands it in the contract’s spot balance. A perp limit order draws on the perp balance, so a freshly funded contract can hold plenty of USDC and still have its orders dropped. Move funds to the perp side first with a USD class transfer (action 7, toPerp: true)—that is why the TraderVault example calls transferUsdClass(perpUsdc, true) before placeLimitOrder.
Setting the account abstraction mode (action 16) is not required to place an order—a new account places orders fine in the default mode. You only need setAbstraction to change how margin works, or to put a builder-fee address into standard mode, which is required for builder-fee accrual.

Respect tick and lot size

limitPx and sz are scaled by 10^8, but they must also conform to the asset’s tick and lot size. Sizes must be rounded to the asset’s szDecimals: if szDecimals is 3, then 1.001 is valid but 1.0001 is not. A size or price with too many decimals is rejected by HyperCore—again, silently. Read szDecimals from the perp-asset-info precompile at 0x000000000000000000000000000000000000080a (PrecompileLib.perpAssetInfo(asset).szDecimals).

Wait for the action to process

Order actions are delayed on-chain by a few seconds (see Action timing). Do not check openOrders in the same block and conclude the order was dropped—give it at least one block confirmation first.

Deploy and call the contract

Compile

Save the contract in a Foundry project and compile it:
forge build

Deploy through a Chainstack endpoint

HyperEVM uses a dual-block architecture: fast 1-second small blocks with a 3M gas limit, and slow 1-minute big blocks with a 30M gas limit. The example contracts here deploy comfortably in small blocks. Larger contracts need big blocks, which you opt into with the HyperCore action {"type": "evmUserModify", "usingBigBlocks": true}. That is an /exchange action, so—unlike the deploy and contract calls, which use your Chainstack endpoint—it goes to Hyperliquid’s public endpoint (https://api.hyperliquid.xyz/exchange); Chainstack does not serve /exchange (see Hyperliquid methods). Deploy with forge create, pointing at your Chainstack HyperEVM endpoint:
forge create src/CoreWriterCaller.sol:CoreWriterCaller \
  --rpc-url YOUR_CHAINSTACK_HYPERLIQUID_ENDPOINT \
  --private-key 0xYOUR_PRIVATE_KEY \
  --broadcast
Replace YOUR_CHAINSTACK_HYPERLIQUID_ENDPOINT with your Chainstack HyperEVM endpoint. The Hyperliquid HyperEVM mainnet uses chain ID 999.

Send an action

Once the contract is deployed and its HyperCore account exists (see Make sure your action lands), call an action with cast. This moves 5 USDC (5,000,000 in perp units) from the contract’s spot wallet to its perp wallet:
cast send YOUR_CONTRACT_ADDRESS \
  "usdClassTransfer(uint64,bool)" 5000000 true \
  --rpc-url YOUR_CHAINSTACK_HYPERLIQUID_ENDPOINT \
  --private-key 0xYOUR_PRIVATE_KEY
To place a limit buy for 0.1 units of asset 0 at a price of 1000, with Gtc time-in-force and no client order ID:
cast send YOUR_CONTRACT_ADDRESS \
  "limitOrder(uint32,bool,uint64,uint64,bool,uint8,uint128)" \
  0 true 100000000000 10000000 false 2 0 \
  --rpc-url YOUR_CHAINSTACK_HYPERLIQUID_ENDPOINT \
  --private-key 0xYOUR_PRIVATE_KEY
Here 100000000000 is 1000 * 10^8 (the price) and 10000000 is 0.1 * 10^8 (the size).

Action timing

CoreWriter actions are not applied the instant the EVM transaction lands. On an L1 block that produces a HyperEVM block, the order of operations is:
  1. The L1 block is built.
  2. The EVM block is built.
  3. EVM-to-Core transfers are processed.
  4. CoreWriter actions are processed.
To prevent latency advantages from bypassing the L1 mempool, order actions and vault transfers sent through CoreWriter are delayed on-chain for a few seconds. This has no practical effect on user experience because the user already waits for at least one block confirmation. These delayed actions appear twice in the L1 explorer: first as an enqueuing, then as the HyperCore execution.

Verify the result

CoreWriter does not return a value to the caller—sendRawAction only emits a log. To confirm an action took effect, read HyperCore state:
  • Query the API /info endpoint (for example, clearinghouseState for perp balances or spotClearinghouseState for spot balances) for the contract address.
  • Read it on-chain through the HyperCore read precompiles at 0x0800 and above.
  • Watch the L1 explorer for the enqueuing and execution entries described above.
If an action does not appear, work through Make sure your action lands: an uninitialized HyperCore account, funds on the wrong (spot vs perp) side, a size that violates szDecimals, or checking before the action delay elapses.

Next steps

Last modified on June 25, 2026