Writing to HyperCore: CoreWriter
CoreWriter turns a contract call into a HyperCore action for the caller's own account, run after the EVM block, with no result returned and no revert on refusal.
CoreWriter lets your contract act on HyperCore: trade, move funds, stake, supply to the lending reserve, split outcome tokens. Precompiles only read. Your contract emits a log, which HyperCore processes as an action after the EVM block is built, so EVM execution never touches HyperCore state.
One contract, one event
Section titled “One contract, one event”CoreWriter sits at 0x3333333333333333333333333333333333333333, with this interface:
interface ICoreWriter { event RawAction(address indexed user, bytes data); function sendRawAction(bytes calldata data) external;}The docs’ attached source burns gas in an empty 400-iteration loop, then does emit RawAction(msg.sender, data) and nothing else. It never validates data, cannot see HyperCore, and cannot fail on anything HyperCore later decides.
| Item | Value |
|---|---|
sendRawAction(bytes) selector |
0x17938e13 |
RawAction topic 0 |
0x8c7f585fb295f7eb1e6aeb8fba61b23a4fe60beda405f0045073b185c74412e3 |
RawAction topic 1 |
the sender, left-padded |
RawAction data |
ABI bytes: offset word, length word, then the payload |
Encoding the payload
Section titled “Encoding the payload”byte 0 version, always 0x01bytes 1 to 3 action id, big-endian uint24bytes 4 on abi.encode(fields...), no function selectorThe fields are the raw ABI encoding of the action’s tuple; abi.encodePacked(uint8(1), bytes3(actionId), abi.encode(...)) builds the whole payload.
Say your contract supplies 10 USDC. The payload is 0x01, 0x00000f (action 15), then three words: operation 0 (Supply), token 0 (USDC) and wei 1,000,000,000 (1e8 per USDC). The call costs about 47,000 gas, whether HyperCore accepts it or not.
Actions run for the caller
Section titled “Actions run for the caller”The action runs for msg.sender’s HyperCore account: a contract’s own, an EOA’s own and, for an API wallet key signing a HyperEVM transaction, the API wallet’s address, not its master’s.
Per the docs, the sender must exist on HyperCore and be activated (a first transfer in, costing 1 quote token) before the EVM block is built; funding it in the same block is too late (activation). CoreWriter does not work for multi-sig users, and converting a user to multi-sig leaves its HyperEVM side controllable by the original wallet.
Actions run after the block, with no reply
Section titled “Actions run after the block, with no reply”Per the docs, the L1 block that produces the EVM block builds it, processes EVM to HyperCore transfers, then CoreWriter actions (measured delays).
sendRawAction never reverts on a HyperCore failure. The HyperEVM is never told. Every refusal returns success, gas is spent, a log is emitted and the money stays put.
Action ids and fields
Section titled “Action ids and fields”| Id | Action | Fields | Units and notes |
|---|---|---|---|
| 1 | Limit order | uint32 asset, bool isBuy, uint64 limitPx, uint64 sz, bool reduceOnly, uint8 encodedTif, uint128 cloid |
limitPx and sz are 1e8 x the human value. TIF 1 Alo, 2 Gtc, 3 Ioc; no FrontendMarket. cloid 0 means none |
| 2 | Vault transfer | address vault, bool isDeposit, uint64 usd |
scale of usd not stated in the docs |
| 3 | Token delegate | address validator, uint64 wei, bool isUndelegate |
HYPE wei; HYPE has 8 decimals on HyperCore |
| 4 | Staking deposit | uint64 wei |
HYPE wei |
| 5 | Staking withdraw | uint64 wei |
HYPE wei |
| 6 | Spot send | address destination, uint64 token, uint64 wei |
HyperCore wei of token (USDC 1e8) |
| 7 | USD class transfer | uint64 ntl, bool toPerp |
scale of ntl not stated in the docs |
| 8 | Finalise EVM contract | uint64 token, uint8 encodedFinalizeEvmContractVariant, uint64 createNonce |
1 Create, 2 FirstStorageSlot, 3 CustomStorageSlot; only Create reads the nonce. See linking |
| 9 | Add API wallet | address wallet, string name |
an empty name makes it the main, unnamed agent |
| 10 | Cancel by oid | uint32 asset, uint64 oid |
|
| 11 | Cancel by cloid | uint32 asset, uint128 cloid |
|
| 12 | Approve builder fee | uint64 maxFeeRate, address builder |
tenths of a basis point: 10 = 0.01% |
| 13 | Send asset | address destination, address subAccount, uint32 source_dex, uint32 destination_dex, uint64 token, uint64 wei |
type(uint32).max selects spot; a non-zero subAccount sends from that sub-account; HyperCore wei |
| 14 | undocumented | observed as three words | exists on mainnet; semantics unknown (below) |
| 15 | Borrow and lend | uint8 encodedOperation, uint64 token, uint64 wei |
0 Supply, 1 Withdraw; wei 0 applies the operation maximally; HyperCore wei (USDC 1e8) |
| 16 | Set abstraction | address user, uint8 abstraction |
1 disabled (Standard, also called Manual), 2 unifiedAccount, 3 portfolioMargin |
| 17 | Outcome operation | uint8 encodedOperation, uint32 question, uint32 outcome, uint64 wei |
0 SplitOutcome, 1 MergeOutcome, 2 MergeQuestion, 3 NegateOutcome. Unused fields must be 0. wei is 1e8 per contract, a multiple of the size; 0 merges the maximum |
Asset ids in actions 1, 10 and 11 follow the API’s numbering (asset ids). A market order from a contract is Ioc with a slippage-bounded limitPx.
For action 17, split and merge ignore question, MergeQuestion ignores outcome, and negate uses both. One split with wei = 100,000,000 minted exactly 1 Long and 1 Short and moved 1 USDC (split, merge, negate).
Action 14 is live but undocumented
Section titled “Action 14 is live but undocumented”It appears in the histogram below; do not build on it. A decoded example from 0x111111a1a0667d36bd57c0a9f569b98057111111, USDH’s mainnet EVM contract per one SDK, carried 0x0100000e then the words [360, 10752298000000, 0], and 360 is USDH’s mainnet token index: probably USDH related.
The docs’ aligned quote asset page describes a CoreWriter action reflecting a deployer’s minted HyperEVM balance onto HyperCore, with no documented id; whether that is action 14 is not established.
Seeing what HyperCore did
Section titled “Seeing what HyperCore did”The EVM receipt says nothing. The HyperCore result shows in three places:
| Where | What it shows |
|---|---|
The explorer endpoint: POST {"type":"userDetails","user":"0x..."} to rpc.hyperliquid.xyz/explorer |
txs, each with a time, a typed action and an error (null when accepted). CoreWriter actions appear under system names; the EVM transactions themselves appear as evmRawTx entries |
| Info requests | The effect: userFills echoes the cloid of an action 1 order; userNonFundingLedgerUpdates shows transfers |
| Precompiles, in a later block | The balance or position that should have moved |
Entries from testnet and mainnet, reformatted:
SystemBoleAction {"operation":"supply","token":0,"wei":50000000000} error: nullSystemBoleAction {"operation":"supply","token":0,"wei":10000000000} error: "Borrow/Lend error: Insufficient spot balance for operation"SystemBoleAction {"operation":"withdraw","token":0,"wei":1000000000} error: "Manual borrow/lend not allowed with portfolio margin"SystemSendAssetAction {"destination":"0x2000...0000","token":0,"wei":10000000000} error: "Insufficient USDC or HYPE balance for token transfer gas."The type name gives the origin: action 15 lands as SystemBoleAction (“Bole” is borrow and lend) with an integer wei, while the HTTP borrowLend action lands as borrowLend with a decimal-string amount.
The numbers
Section titled “The numbers”| Parameter | Value |
|---|---|
| Address | 0x3333333333333333333333333333333333333333 |
| Version byte | 0x01 |
| Action id | 3 bytes, big-endian |
| Gas burned before the log | about 25,000 (docs text); the attached source says about 20k |
| Gas for a basic call | about 47,000 (docs); measured 47,889 on mainnet |
| Explorer history per user | last 300 actions |
| Documented ids | 1 to 13, 15 to 17 |
Mainnet action histogram, EVM blocks 43,380,826 to 46,380,826 (3,000,001 blocks, about 35 days, zero gaps). The window also held a one-off fuzz sweep of ids 18 to 29 and six garbage ids.
| Id | Count | Id | Count |
|---|---|---|---|
| 13 send asset | 249,116 | 12 builder fee | 626 |
| 1 limit order | 9,309 | 2 vault transfer | 217 |
| 6 spot send | 6,403 | 16 set abstraction | 203 |
| 7 USD class transfer | 2,305 | 4 staking deposit | 199 |
| 9 add API wallet | 2,070 | 5 staking withdraw | 188 |
| 3 token delegate | 1,395 | 11 cancel by cloid | 129 |
| 10 cancel by oid | 1,235 | 14 undocumented | 25 |
| 15 borrow and lend | 13 | 17 outcome operation | 4 |
| 8 finalise EVM contract | 0 |
Orders placed for you
Section titled “Orders placed for you”A HyperEVM app trading for you places real HyperCore orders from its contract’s own account, as Ioc with a price limit (no FrontendMarket). On mainnet they executed 0.35 to 0.51 s after their EVM block; a refused one still shows a successful EVM transaction.
A minimal sender, and golden vectors
Section titled “A minimal sender, and golden vectors”// SPDX-License-Identifier: MITpragma solidity ^0.8.28;
interface ICoreWriter { function sendRawAction(bytes calldata data) external;}
/// Acts for this contract's own HyperCore account. No access control here:/// gate these functions in real code. Nothing below can tell whether HyperCore accepted the action.contract CoreWriterExample { ICoreWriter constant CORE_WRITER = ICoreWriter(0x3333333333333333333333333333333333333333);
uint8 constant TIF_IOC = 3; uint8 constant OP_SUPPLY = 0;
/// Version byte 0x01, then the action id as a big-endian uint24, then the ABI fields. function _send(uint24 actionId, bytes memory fields) internal { CORE_WRITER.sendRawAction(abi.encodePacked(uint8(1), bytes3(actionId), fields)); }
/// Action 1. limitPx and sz are 1e8 x the human value. No FrontendMarket: /// a market order is Ioc with a bounded limit price. function buyIoc(uint32 asset, uint64 limitPx1e8, uint64 sz1e8, uint128 cloid) external { _send(1, abi.encode(asset, true, limitPx1e8, sz1e8, false, TIF_IOC, cloid)); }
/// Action 15, supply. wei is HyperCore wei of the token: USDC is 1e8. function supply(uint64 token, uint64 coreWei) external { _send(15, abi.encode(OP_SUPPLY, token, coreWei)); }}Golden vectors from mainnet payloads, which your encoder must reproduce byte for byte:
Supply 4,014.92350600 USDC (block 45,041,472)0100000f0000000000000000000000000000000000000000000000000000000000000000 op 0, Supply0000000000000000000000000000000000000000000000000000000000000000 token 0, USDC0000000000000000000000000000000000000000000000000000005d7acf1688 wei 401,492,350,600
Withdraw 10 USDC (block 46,253,547)0100000f0000000000000000000000000000000000000000000000000000000000000001 op 1, Withdraw0000000000000000000000000000000000000000000000000000000000000000 token 0, USDC000000000000000000000000000000000000000000000000000000003b9aca00 wei 1,000,000,000HyperCore refused the second (its sender was in portfolio margin mode); the vector tests the encoder, not the outcome.
- Record an intent before calling, and settle it only after reading HyperCore state in a later block (patterns).
- Set the abstraction mode with action 16 before the contract holds any position.
- Put a
cloidon every action 1 order so its fill can be matched later.
Refusals and drops on chain
Section titled “Refusals and drops on chain”| Behaviour | Detail |
|---|---|
| Silent refusals | EVM status 1 with a HyperCore error on real transactions: the errors above, and “Invalid number of decimals” (footguns) |
| Action 17 with a non-zero unused field | Dropped, per the docs, not rejected |
| Spot orders on one testnet account | Silently dropped even above the minimum notional, while the identical perp order succeeded (small sample). On mainnet, cloid-matched fills in the delay study included spot pairs, so spot orders do execute there |
| Portfolio margin | Rejects action 15 |
| Leaving a unified account | Refused with open positions, open orders or active TWAPs: “Cannot disable unified account with open positions, open orders, or active TWAP orders” (observed twice) |
| Action 16 targets | The docs allow the master or a sub-account; all 123 distinct senders of the 203 action 16 emissions in the scan targeted themselves, and the 30 most recent were all contracts whose mode took effect |
| Explorer history | Capped at 300 actions per user; one busy account recorded 278 actions in half an hour. Log the result yourself when you see it |
| Delayed actions | Per the docs, the explorer lists them twice: the enqueuing, then the execution |
For when each action lands, see execution order and timing.