Reading HyperCore: precompiles
Read precompiles are 21 fixed addresses, 0x800 to 0x814, that return HyperCore state as of the EVM block's construction, and only at the latest block.
Lending against a HyperCore position, pricing a vault share or settling on an outcome all need HyperCore facts. Your contract reads balances, prices, positions and lending rates with one staticcall to a precompile.
Call with raw ABI, no selector
Section titled “Call with raw ABI, no selector”staticcall the fixed address with calldata abi.encode(args) and no function selector: a selector would be read as part of the first argument, answering about the wrong thing or failing. Output decodes straight into the structs in the docs’ L1Read.sol; 0x809 takes empty calldata.
Per the docs, every read in one EVM block sees the same snapshot: the latest HyperCore state when that block was constructed.
Say you read a USDC spot balance through 0x801: 64 bytes in (user, token) and 96 out, so it costs 2000 + 65 * 160 = 12,400 gas. A 100 USDC balance returns total 10,000,000,000, in HyperCore wei.
What each address returns
Section titled “What each address returns”| Address | Name | Input | Output |
|---|---|---|---|
0x800 |
position |
address user, uint16 perp |
(int64 szi, uint64 entryNtl, int64 isolatedRawUsd, uint32 leverage, bool isIsolated) |
0x801 |
spotBalance |
address user, uint64 token |
(uint64 total, uint64 hold, uint64 entryNtl) |
0x802 |
userVaultEquity |
address user, address vault |
(uint64 equity, uint64 lockedUntilTimestamp) |
0x803 |
withdrawable |
address user |
(uint64 withdrawable) |
0x804 |
delegations |
address user |
(address validator, uint64 amount, uint64 lockedUntilTimestamp)[] |
0x805 |
delegatorSummary |
address user |
(uint64 delegated, uint64 undelegated, uint64 totalPendingWithdrawal, uint64 nPendingWithdrawals) |
0x806 |
markPx |
uint32 index |
uint64 |
0x807 |
oraclePx |
uint32 index |
uint64 |
0x808 |
spotPx |
uint32 index |
uint64 |
0x809 |
l1BlockNumber |
none | uint64 |
0x80a |
perpAssetInfo |
uint32 perp |
(string coin, uint32 marginTableId, uint8 szDecimals, uint8 maxLeverage, bool onlyIsolated) |
0x80b |
spotInfo |
uint32 spot |
(string name, uint64[2] tokens) |
0x80c |
tokenInfo |
uint32 token |
(string name, uint64[] spots, uint64 deployerTradingFeeShare, address deployer, address evmContract, uint8 szDecimals, uint8 weiDecimals, int8 evmExtraWeiDecimals) |
0x80d |
tokenSupply |
uint32 token |
(uint64 maxSupply, uint64 totalSupply, uint64 circulatingSupply, uint64 futureEmissions, (address user, uint64 balance)[] nonCirculatingUserBalances) |
0x80e |
bbo |
uint32 asset |
(uint64 bid, uint64 ask) |
0x80f |
accountMarginSummary |
uint32 perp_dex_index, address user |
(int64 accountValue, uint64 marginUsed, uint64 ntlPos, int64 rawUsd) |
0x810 |
coreUserExists |
address user |
(bool exists) |
0x811 |
borrowLendUserState |
address user, uint64 token |
((uint64 basis, uint64 value) borrow, (uint64 basis, uint64 value) supply) |
0x812 |
borrowLendReserveState |
uint64 token |
(uint64 borrowYearlyRateBps, uint64 supplyYearlyRateBps, uint64 balance, uint64 utilizationBps, uint64 oraclePx, uint64 ltvBps, uint64 totalSupplied, uint64 totalBorrowed) |
0x813 |
position2 |
address user, uint32 perp |
same as 0x800 |
0x814 |
outcomeStatus |
uint32 outcome |
(uint8 status, uint64 settledValue, uint32 question) |
Sibling widths differ: 0x812 takes the token as uint64, 0x80c and 0x80d as uint32. Each encodes to one 32-byte word, so the width limits a wrapper’s range, not the layout. 0x80e takes a perp index or 10000 plus a spot pair index; 0x80f takes the perp dex index, then the user.
Scaling the answers
Section titled “Scaling the answers”| Value | Rule |
|---|---|
markPx, oraclePx (perp) |
divide by 10^(6 - szDecimals) of the perp |
spotPx |
divide by 10^(8 - szDecimals) of the base token |
Token amounts (0x801, 0x811, 0x812) |
HyperCore wei of the token; measured 1e8 for USDC |
Rates, utilisation, LTV (0x812) |
basis points |
settledValue (0x814) |
100,000,000 = 1.0 |
bbo |
not stated in the docs; check against the info API before relying on it |
A live 0x812 read for USDC returned borrowYearlyRateBps 500, supplyYearlyRateBps 303, utilizationBps 6736 and totalSupplied 44,172,678,296,289,157 (441,726,782.96 USDC at 1e8). Supply rate is borrow rate times utilisation times 0.9, the protocol keeping 10 per cent of borrow interest: 500 * 0.6736 * 0.9 = 303.1, reported as 303. Against the API the identity holds to 9 decimal places.
Outcome ids and status
Section titled “Outcome ids and status”0x801 (token) and 0x808 (index) also accept an encoded outcome asset id, answering for that side’s outcome token and spot market. Values below 100,000,000 keep their old meaning.
id = 100,000,000 + 10 * outcome + side side 0 or 1Testnet outcome 20351, for example, is Long 100203510 and Short 100203511 (asset ids). 0x814 takes the raw outcome id:
status |
Meaning |
|---|---|
| 0 | never existed |
| 1 | active |
| 2 | settled; settledValue readable |
| 3 | settled and pruned; settledValue gone for good |
question is type(uint32).max when the outcome is standalone or the question unavailable. A contract settling against an outcome must store settledValue before pruning (settlement).
gas = 2000 + 65 * (input_len + output_len) lengths in bytes| Call | Input | Output | Formula gas |
|---|---|---|---|
0x810 coreUserExists |
32 | 32 | 6,160 |
0x801 spotBalance |
64 | 96 | 12,400 |
0x811 borrowLendUserState |
64 | 128 | 14,480 |
0x812 borrowLendReserveState |
32 | 256 | 20,720 |
An invalid input (an unknown asset, a bad vault address) errors and consumes all gas passed into its call frame: measured InvalidTransaction(PrecompileOutOfGas(50000000)), the whole 50M allowance. One stale id in a loop burns the transaction, so cap every call; one production library caps each read at twice its formula cost and gives dynamic outputs such as tokenInfo a flat 120,000.
The numbers
Section titled “The numbers”| Parameter | Value |
|---|---|
| Assigned addresses | 0x800 to 0x814 on mainnet and testnet |
| Unassigned range checked | 0x815 to 0x8ff, empty success (2026-08-17) |
| Gas | 2000 + 65 * (input_len + output_len) |
| Bad input | consumes all forwarded gas |
| Outcome asset id | 100,000,000 + 10 * outcome + side |
settledValue scale |
100,000,000 = 1.0 |
| Perp price scale | 10^(6 - szDecimals) |
| Spot price scale | 10^(8 - base szDecimals) |
| Block tag | latest only |
Prices are as of your block
Section titled “Prices are as of your block”A HyperEVM protocol pricing your collateral from 0x806 to 0x808 sees HyperCore as of its own block’s construction, not a live tick. It cannot re-read a past price or exchange rate unless it stored it at the time.
Reading safely from a contract
Section titled “Reading safely from a contract”A minimal capped read that fails soft instead of reverting:
// SPDX-License-Identifier: MITpragma solidity ^0.8.28;
contract SpotBalanceReader { address constant SPOT_BALANCE = 0x0000000000000000000000000000000000000801;
// Formula: 2000 + 65 * (64 bytes in + 96 bytes out) = 12,400. Cap at about twice that. uint256 constant SPOT_BALANCE_GAS = 25_000;
struct SpotBalance { uint64 total; // HyperCore wei of the token: USDC is 1e8 uint64 hold; uint64 entryNtl; }
/// Returns ok = false for a bad input, an unassigned address or a short answer, /// having spent at most SPOT_BALANCE_GAS. Raw abi.encode, no selector. function trySpotBalance(address user, uint64 token) external view returns (bool ok, SpotBalance memory bal) { (bool success, bytes memory out) = SPOT_BALANCE.staticcall{gas: SPOT_BALANCE_GAS}(abi.encode(user, token)); if (success && out.length == 96) { bal = abi.decode(out, (SpotBalance)); ok = true; } }}The same call from the command line (the docs’ own example reads oraclePx for index 3 on testnet):
cast call 0x0000000000000000000000000000000000000807 \ 0x0000000000000000000000000000000000000000000000000000000000000003 \ --rpc-url https://rpc.hyperliquid-testnet.xyz/evm- The docs’
L1Read.solforwards all remaining gas andrequires success. It describes the interface, not production code; route every call through one capped function. - Deployer stake is
0x805, not0x804.delegated + undelegatedin the deployer’s own staking account meets the requirement, andtotalPendingWithdrawal > 0warns that stake is leaving (deployer economics). - Never price shares off
0x812 balance. It is nottotalSupplied - totalBorrowed: the gap, the protocol’s retained buffer, was 46.56877103 USDC and growing across three reads 3 seconds apart. Use the account’s own0x811 supply.value. - Off-chain decoding is slicing 32-byte words; every struct is static except
0x804and0x80ato0x80d.
Where reads fail or mislead
Section titled “Where reads fail or mislead”| Behaviour | Evidence | What to do |
|---|---|---|
0x811 reverts when the account has no borrow or lend record, rather than returning zeros |
PrecompileError on both networks, even for a HyperCore user holding 31,989 USDC of spot |
Treat a failure as a zero position until the first supply lands; a fresh vault reading it unguarded in totalAssets() cannot take its first deposit |
No history: answers only at latest |
An eth_call at any explicit block number, even the current head, fails with PrecompileOutOfGas(50000000); measured at offsets of 0 to 10,000 blocks, on archive nodes that serve historical eth_getCode |
Sample forward; there is no backfill |
| Unassigned addresses return empty success | On 2026-08-17, 0x815 and above returned 0x with success; a live precompile returns data or PrecompileError |
Check the output length, or a typo’d address decodes as garbage |
| Revisions arrive as sibling addresses | 0x813 position2 widens perp to uint32 (0x800’s uint16 stops at 65,535) and 0x800 is unchanged; 0x814 appeared on testnet before it was documented |
Watch for 0x815. Never narrow an index below uint32: outcome asset ids start at 100,000,000 |
| Stale by execution | A read is as of block construction and a CoreWriter action lands after the block, so a read and an action in one transaction never see the same HyperCore | See execution order |
| No precompile returns the account abstraction mode | Use the userAbstraction info request (account modes) |
The docs agree there is no history: read precompile results are recorded only for calls actually made in each block, and anything else needs a full L1 replay.
For what 0x811 and 0x812 describe, see borrowing and lending.