Skip to content
New guideAgent Cookbook 12 copy-ready prompts for AI coding agents.See recipes
All blog posts
HyperEVM developer guide: RPC, chain ID 999, big blocks, CoreWriter and precompiles

HyperEVM developer guide: RPC, chain ID 999, big blocks, CoreWriter and precompiles

By Ben Chatwin36min read

HyperEVM accepts the same JSON-RPC calls as any EVM chain, and most Ethereum tooling works against it unchanged. The differences show up after deployment. Contract deploys fail because they don't fit in a 3M-gas block. An eth_getLogs backfill that worked yesterday stops with Response is too big. A CoreWriter order returns status 1 and never reaches the book. A precompile read that works at latest fails at a block number.

This guide covers those cases with code. We wrote a small TypeScript and Solidity toolkit for HyperEVM, and every snippet ran against mainnet on 3 November 2026, through Dwellir's HyperEVM endpoint and the official public RPC. Where the two endpoints behave differently, the guide shows both results. For what HyperEVM and HyperCore are and how they share HyperBFT consensus, start with What is Hyperliquid. This page assumes you know that and want to ship.

What you will learn

  • Connect to HyperEVM on chain ID 999 and confirm which endpoint features you have
  • Tell small blocks from big blocks, and route a large deployment to big blocks
  • Backfill eth_getLogs across any range without hitting range, size or result caps
  • Read HyperCore perp positions and oracle prices from TypeScript and from a Solidity contract
  • Encode CoreWriter actions, simulate them and check why one didn't land
  • Trace HyperEVM transactions, including calls into precompiles

Prerequisites

  • Node.js 22.18 or later, which runs TypeScript files directly. Run node --version to check. The samples were run on Node 24.
  • A project with ES modules and viem 2.x: npm init -y && npm pkg set type=module && npm i viem. The samples were tested with viem 2.57.
  • A Dwellir API key, exported as DWELLIR_API_KEY. Create one in the dashboard. eth_getLogs, debug and trace need a paid plan, which starts with Developer at $49 a month.
  • For the Solidity sections, any compiler targeting cancun. The contracts here were compiled with solc 0.8.28 and run through eth_call state overrides, so nothing in this guide deploys a contract or sends a transaction.
  • Working knowledge of Solidity ABI encoding and of async TypeScript.

HyperEVM RPC endpoints and chain ID 999

HyperEVM mainnet is chain ID 999 (0x3e7). Testnet is 998, served at https://rpc.hyperliquid-testnet.xyz/evm. The official mainnet endpoint is https://rpc.hyperliquid.xyz/evm. It runs Hyperliquid's own EVM server and has no WebSocket.

Dwellir's HyperEVM endpoint runs a Reth-based archive node; every method it serves is listed in the HyperEVM JSON-RPC reference. That changes more than the URL:

Measured 2026-11-03Official public RPCDwellir HyperEVM
URLhttps://rpc.hyperliquid.xyz/evmhttps://api-hyperliquid-mainnet-evm.n.dwellir.com/YOUR_API_KEY
web3_clientVersionhyperliquid evm Mainnetreth/v0.1.0-2c6525d
WebSocket and eth_subscribeNoYes, wss:// on the same host
State at a past block (eth_call, eth_getBalance)Ignores the block number and answers from latest stateArchive back to block 1 (18 Feb 2025)
eth_getLogs range1,000 blocks and 1 MiB per response500 blocks (Developer), 10,000 (Growth and up)
debug_* and trace_*Method not foundYes, paid plans
HyperCore system transactionsLeft out of blocks and logs; listed by eth_getSystemTxsByBlockNumberIncluded in blocks and logs
eth_bigBlockGasPrice, eth_usingBigBlocksYesMethod not found

Check the chain before you sign anything:

Bash
curl -s https://api-hyperliquid-mainnet-evm.n.dwellir.com/$DWELLIR_API_KEY \
  -H 'content-type: application/json' \
  -d '{"jsonrpc":"2.0","id":1,"method":"eth_chainId","params":[]}'
JSON
{"jsonrpc":"2.0","id":1,"result":"0x3e7"}

The rest of the guide uses one viem client. Save it as client.ts:

TypeScript
// client.ts - shared HyperEVM client for the guide samples
import { createPublicClient, http, defineChain } from 'viem';

const key = process.env.DWELLIR_API_KEY;
if (!key) throw new Error('Set DWELLIR_API_KEY');

export const hyperEvm = defineChain({
  id: 999,
  name: 'HyperEVM',
  nativeCurrency: { name: 'HYPE', symbol: 'HYPE', decimals: 18 },
  rpcUrls: {
    default: { http: [`https://api-hyperliquid-mainnet-evm.n.dwellir.com/${key}`] },
  },
});

export const client = createPublicClient({
  chain: hyperEvm,
  transport: http(undefined, { batch: true, retryCount: 3 }),
});

// No automatic retries: lets callers react to range and result-size errors themselves.
export const logsClient = createPublicClient({
  chain: hyperEvm,
  transport: http(undefined, { retryCount: 0, timeout: 60_000 }),
});

batch: true packs concurrent calls into one HTTP request, which matters when you scan hundreds of blocks. The second client turns retries off. viem retries JSON-RPC error -32005 by default, and on HyperEVM that code usually means a range limit, so retrying the same request three times only adds delay.

Confirm the connection with 01-connect.ts:

TypeScript
// 01-connect.ts - confirm you are on HyperEVM mainnet
import { client } from './client.ts';

const [chainId, head, gasPrice] = await Promise.all([
  client.getChainId(),
  client.getBlockNumber(),
  client.getGasPrice(),
]);

if (chainId !== 999) throw new Error(`Wrong chain: ${chainId}`);
console.log({ chainId, head, gasPriceGwei: Number(gasPrice) / 1e9 });
Bash
node 01-connect.ts
Text
{ chainId: 999, head: 50254353n, gasPriceGwei: 0.21 }

Important: the public RPC answers a historical state request with current state. Hyperliquid's docs say its eth_call, eth_getBalance, eth_getCode and eth_getStorageAt support only the latest block, and hl-node issue #158 reports that it returns no error when you pass an older one. A quick check reproduces it: totalSupply() on WHYPE (0x5555...5555) at block 10,000,000 returned 2,947,073 WHYPE from the public RPC, the same as at latest. Dwellir's archive node returned 9,235,597 WHYPE, the supply at that block.

Small blocks vs big blocks

HyperEVM splits throughput between two block types that share one increasing sequence of block numbers. Per Hyperliquid's dual-block architecture docs:

Block typeCadenceGas limitUsed for
SmallEvery 1 second3,000,000Default for all transactions
BigEvery 1 minute30,000,000Large deployments and heavy transactions, opt-in

A block's gasLimit tells you which type it is. This script classifies the last 300 blocks:

TypeScript
// 02-block-types.ts - classify recent blocks as small (3M gas) or big (30M gas)
import { client } from './client.ts';

const SCAN = 300n;
const head = await client.getBlockNumber();

const blocks = await Promise.all(
  Array.from({ length: Number(SCAN) }, (_, i) =>
    client.getBlock({ blockNumber: head - BigInt(i) }),
  ),
);

const big = blocks.filter((b) => b.gasLimit === 30_000_000n);
const small = blocks.filter((b) => b.gasLimit === 3_000_000n);
const other = blocks.length - big.length - small.length;

console.log(`scanned ${blocks.length} blocks back from ${head}`);
console.log(`small (3M gas): ${small.length}, big (30M gas): ${big.length}, other: ${other}`);
for (const b of big.sort((a, z) => Number(a.number - z.number))) {
  console.log(`big block ${b.number} at ${new Date(Number(b.timestamp) * 1000).toISOString()} txs=${b.transactions.length} gasUsed=${b.gasUsed}`);
}
Text
scanned 300 blocks back from 50254353
small (3M gas): 295, big (30M gas): 5, other: 0
big block 50254095 at 2026-11-03T06:24:00.000Z txs=4 gasUsed=632185
big block 50254156 at 2026-11-03T06:25:00.000Z txs=7 gasUsed=3052197
big block 50254217 at 2026-11-03T06:26:00.000Z txs=1 gasUsed=26420
big block 50254278 at 2026-11-03T06:27:00.000Z txs=4 gasUsed=3237920
big block 50254339 at 2026-11-03T06:28:00.000Z txs=4 gasUsed=3419134

With batching on, the 300 requests took 0.35 seconds. Big blocks landed every 61 block numbers, on the minute. Three of the five used more than 3M gas, so their transactions could not have fit in a small block.

A big block has the same timestamp as the small block next to it. Over a 75-second WebSocket newHeads subscription, blocks 50,254,887 (small) and 50,254,888 (big) both carried timestamp 1793687820. Key your tables on block number, not timestamp.

Send a deployment to big blocks

Big-block routing is a flag on the sender's HyperCore account. You set it with the HyperCore action evmUserModify, signed like any other exchange action. You don't set it with an EVM transaction. Hyperliquid's Python SDK wraps it (basic_evm_use_big_blocks.py):

Python
# Not run for this guide: it signs and submits a HyperCore action.
from hyperliquid.exchange import Exchange
from hyperliquid.utils import constants
from eth_account import Account

wallet = Account.from_key(DEPLOYER_PRIVATE_KEY)
exchange = Exchange(wallet, constants.MAINNET_API_URL)

exchange.use_big_blocks(True)    # {"type": "evmUserModify", "usingBigBlocks": true}
# ... deploy the contract from this address over HyperEVM RPC ...
exchange.use_big_blocks(False)   # back to 1-second small blocks

Before you deploy this way, check three things:

  1. The deployer must already exist as a HyperCore user before it can send the action. An EOA becomes one when it receives a HyperCore asset such as USDC.
  2. The flag stays set. Every later transaction from that address waits up to a minute for a big block until you turn it off.
  3. eth_bigBlockGasPrice (the next big block's base fee) and eth_usingBigBlocks (the flag for an address) exist only on the official RPC. Dwellir's Reth node returned -32601 Method not found for both on 3 November 2026. Read them from rpc.hyperliquid.xyz/evm and send everything else through your main endpoint.

eth_getLogs and indexing limits

Hyperliquid's JSON-RPC docs list a 50-block range and 4 topics for eth_getLogs on the public RPC. On 3 November 2026 the public RPC behaved differently:

Request (WHYPE logs)Public RPC result
50, 51, 200, 500 and 1,000 blocksAccepted
1,001 blocks-32008 Response is too big, Exceeded max limit of 1048576
2,000 blocks-32602 query exceeds max block range 1000
Requests in quick succession-32005 rate limited

The 1 MiB response cap is the one to plan around. hl-node issue #65 shows it failing on a single busy block with no address filter. A single-block query can't be split any further, so a full-chain indexer cannot rely on the public endpoint.

Dwellir's limits are set per plan: 500 blocks per request on Developer, 10,000 on Growth and above (rate limits). These results are from a key on a 10,000-block plan:

Request on DwellirResult
10,000 blocks, WHYPE address filter14,791 logs, 9.3 MB, 2.7 s
10,001 blocks-32005 eth_getLogs range of 10001 blocks exceeds the 10000-block limit for this plan
1,000 blocks, no filter17,834 logs, 13.6 MB, 6.3 s
5,000 blocks, no filter-32602 query exceeds max results 20000, retry with the range 50249371-50250737

Address and topic filters don't change the allowed range. The result cap's error message includes a range that fits. This helper uses that range when it's given, halves the window for any other limit error, and continues from the last block it fetched:

TypeScript
// getLogsChunked.ts - eth_getLogs over any range, split to fit provider limits
import { formatLog, type Log, type RpcLog, type Hex } from 'viem';
import { logsClient as client } from './client.ts';

const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms));

type Filter = { address?: Hex | Hex[]; topics?: (Hex | Hex[] | null)[] };

export async function getLogsChunked(
  filter: Filter,
  fromBlock: bigint,
  toBlock: bigint,
  chunk = 10_000n, // Dwellir Growth and up: 10,000. Developer: 500. Public RPC: 1,000.
): Promise<Log[]> {
  const out: Log[] = [];
  let start = fromBlock;
  let size = chunk;
  while (start <= toBlock) {
    const end = start + size - 1n > toBlock ? toBlock : start + size - 1n;
    try {
      const logs = await client.request({
        method: 'eth_getLogs',
        params: [{ ...filter, fromBlock: `0x${start.toString(16)}`, toBlock: `0x${end.toString(16)}` }],
      });
      out.push(...(logs as RpcLog[]).map((l) => formatLog(l)));
      start = end + 1n;
      size = chunk; // grow back after a successful window
    } catch (err) {
      const msg = String((err as Error).message);
      // Rate limited (the public RPC answers -32005 "rate limited"): wait, then retry as is.
      if (/rate limit/i.test(msg)) {
        await sleep(5_000);
        continue;
      }
      // Dwellir's result cap names a range that fits: "retry with the range A-B".
      const hint = msg.match(/retry with the range (\d+)-(\d+)/);
      if (hint) {
        size = BigInt(hint[2]) - BigInt(hint[1]) + 1n;
        continue;
      }
      // Other range or size limits, including viem's own 10 MiB response cap
      // ("exceeded the size limit"): halve the window and retry the same start block.
      if (size > 1n && /exceed|too big|max results|block range|size limit/i.test(msg)) {
        size = size / 2n;
        continue;
      }
      throw err;
    }
  }
  return out;
}

Test it on 3,000 blocks with no filter, which forces several result-cap splits:

TypeScript
// 03-getlogs-test.ts
import { client } from './client.ts';
import { getLogsChunked } from './getLogsChunked.ts';

const head = await client.getBlockNumber();
const t0 = Date.now();
const logs = await getLogsChunked({}, head - 2999n, head);
console.log(`${logs.length} logs over 3,000 blocks in ${Date.now() - t0} ms`);
Text
57798 logs over 3,000 blocks in 4591 ms

An earlier version that only halved the window and kept viem's default retries took 67 seconds for the same scan. viem also refuses any HTTP response over 10 MiB on the client side, and a 1,000-block unfiltered window can come close, so the helper treats that error like a provider limit and halves the window.

Logs from HyperCore system transactions

When HyperCore moves a token to HyperEVM, the protocol adds a system transaction to the EVM block. It has no real signature, its sender is the token's system address (0x2000... plus the token index, or 0x2222...2222 for HYPE), and it calls transfer on the linked ERC-20. The official RPC leaves these out of eth_getBlockByNumber and eth_getLogs and serves them through eth_getSystemTxsByBlockNumber. Dwellir's Reth node includes them in the block.

In block 50,253,840, the token at 0xbe67...7907 had 2 Transfer logs on the public RPC and 3 on Dwellir. The extra log came from a system transaction at index 0, sent from 0x2000...00dd with gas price 0. The other two logs had log index 1 and 7 on the public RPC and 2 and 8 on Dwellir. If you index token balances from the public RPC's blocks and eth_getLogs alone, you miss every Core-to-EVM deposit. If you compare data across providers, don't use (txHash, logIndex) as the key.

Read HyperCore state like perp positions from a contract

HyperEVM contracts read HyperCore through read precompiles at 0x0000000000000000000000000000000000000800 and up. They return perp positions, spot balances, vault equity, staking state, prices and market metadata. Per Hyperliquid's docs, the values match HyperCore state when the EVM block is built. The full list, from the official L1Read.sol attached to that page:

AddressPrecompileInput (abi.encode)Returns
0x...0800position(address user, uint16 perp)(int64 szi, uint64 entryNtl, int64 isolatedRawUsd, uint32 leverage, bool isIsolated)
0x...0801spotBalance(address user, uint64 token)(uint64 total, uint64 hold, uint64 entryNtl)
0x...0802userVaultEquity(address user, address vault)(uint64 equity, uint64 lockedUntilTimestamp)
0x...0803withdrawable(address user)(uint64 withdrawable)
0x...0804delegations(address user)Delegation[]
0x...0805delegatorSummary(address user)delegated, undelegated, pending withdrawals
0x...0806markPx(uint32 perp)uint64
0x...0807oraclePx(uint32 perp)uint64
0x...0808spotPx(uint32 spot)uint64
0x...0809l1BlockNumbernoneuint64
0x...080aperpAssetInfo(uint32 perp)coin, marginTableId, szDecimals, maxLeverage, onlyIsolated
0x...080bspotInfo(uint32 spot)name, token pair
0x...080ctokenInfo(uint32 token)name, decimals, deployer, linked EVM contract
0x...080dtokenSupply(uint32 token)max, total and circulating supply
0x...080ebbo(uint32 asset)(uint64 bid, uint64 ask)
0x...080faccountMarginSummary(uint32 perpDexIndex, address user)(int64 accountValue, uint64 marginUsed, uint64 ntlPos, int64 rawUsd)
0x...0810coreUserExists(address user)bool
0x...0811borrowLendUserState(address user, uint64 token)borrow and supply basis and value
0x...0812borrowLendReserveState(uint64 token)rates, utilization, totals
0x...0813position2(address user, uint32 perp)same as position
0x...0814outcomeStatus(uint32 outcome)status, settled value, question

Precompiles take raw ABI-encoded arguments with no 4-byte function selector. Each call costs 2000 + 65 * (input_len + output_len) gas, so a position read (64 bytes in, 160 out) costs 16,560. An invalid input, such as an unknown asset index, reverts and consumes all the gas passed to the call. position takes a uint16 perp index, which can't hold builder-deployed (HIP-3) perp IDs of 100,000 and up, so use position2 with its uint32 index for those.

The quickest test is a raw eth_call. This reads the oracle price for perp 0, which is BTC:

Bash
curl -s https://api-hyperliquid-mainnet-evm.n.dwellir.com/$DWELLIR_API_KEY \
  -H 'content-type: application/json' \
  -d '{"jsonrpc":"2.0","id":1,"method":"eth_call","params":[{"to":"0x0000000000000000000000000000000000000807","data":"0x0000000000000000000000000000000000000000000000000000000000000000"},"latest"]}'
JSON
{"jsonrpc":"2.0","id":1,"result":"0x00000000000000000000000000000000000000000000000000000000000bb748"}

0xbb748 is 767,816. Perp prices are scaled by 10^(6 - szDecimals), and BTC has szDecimals 5, so the price is $76,781.60. Spot prices use 10^(8 - szDecimals) of the base token.

From TypeScript

04-precompiles.ts reads market metadata, prices and one account's BTC position, and does the scaling for you:

TypeScript
// 04-precompiles.ts - read HyperCore state through the read precompiles
import { encodeAbiParameters, decodeAbiParameters, type Hex } from 'viem';
import { client } from './client.ts';

const PRECOMPILE = {
  position: '0x0000000000000000000000000000000000000800',
  markPx: '0x0000000000000000000000000000000000000806',
  oraclePx: '0x0000000000000000000000000000000000000807',
  l1BlockNumber: '0x0000000000000000000000000000000000000809',
  perpAssetInfo: '0x000000000000000000000000000000000000080a',
  accountMarginSummary: '0x000000000000000000000000000000000000080f',
} as const;

// Precompiles take raw ABI-encoded arguments: no 4-byte function selector.
async function readPrecompile(to: Hex, data: Hex): Promise<Hex> {
  const { data: out } = await client.call({ to, data, blockTag: 'latest' });
  if (!out) throw new Error(`empty result from ${to}`);
  return out;
}

const user = process.argv[2] as Hex;
const perp = 0; // perp index 0 = BTC

const [info] = decodeAbiParameters(
  [{ type: 'tuple', components: [
    { name: 'coin', type: 'string' }, { name: 'marginTableId', type: 'uint32' },
    { name: 'szDecimals', type: 'uint8' }, { name: 'maxLeverage', type: 'uint8' },
    { name: 'onlyIsolated', type: 'bool' },
  ] }],
  await readPrecompile(PRECOMPILE.perpAssetInfo, encodeAbiParameters([{ type: 'uint32' }], [perp])),
);

const [oracleRaw] = decodeAbiParameters([{ type: 'uint64' }],
  await readPrecompile(PRECOMPILE.oraclePx, encodeAbiParameters([{ type: 'uint32' }], [perp])));
const [markRaw] = decodeAbiParameters([{ type: 'uint64' }],
  await readPrecompile(PRECOMPILE.markPx, encodeAbiParameters([{ type: 'uint32' }], [perp])));
const [l1Block] = decodeAbiParameters([{ type: 'uint64' }],
  await readPrecompile(PRECOMPILE.l1BlockNumber, '0x'));

// Perp prices come back scaled by 10^(6 - szDecimals).
const pxScale = 10 ** (6 - info.szDecimals);

const [pos] = decodeAbiParameters(
  [{ type: 'tuple', components: [
    { name: 'szi', type: 'int64' }, { name: 'entryNtl', type: 'uint64' },
    { name: 'isolatedRawUsd', type: 'int64' }, { name: 'leverage', type: 'uint32' },
    { name: 'isIsolated', type: 'bool' },
  ] }],
  await readPrecompile(PRECOMPILE.position,
    encodeAbiParameters([{ type: 'address' }, { type: 'uint16' }], [user, perp])),
);

const [margin] = decodeAbiParameters(
  [{ type: 'tuple', components: [
    { name: 'accountValue', type: 'int64' }, { name: 'marginUsed', type: 'uint64' },
    { name: 'ntlPos', type: 'uint64' }, { name: 'rawUsd', type: 'int64' },
  ] }],
  await readPrecompile(PRECOMPILE.accountMarginSummary,
    encodeAbiParameters([{ type: 'uint32' }, { type: 'address' }], [0, user])),
);

console.log({
  hyperCoreBlock: l1Block,
  market: info.coin,
  szDecimals: info.szDecimals,
  oraclePx: Number(oracleRaw) / pxScale,
  markPx: Number(markRaw) / pxScale,
  user,
  // szi is in units of 10^szDecimals; USD values are in 10^6 units
  positionSize: Number(pos.szi) / 10 ** info.szDecimals,
  entryNotionalUsd: Number(pos.entryNtl) / 1e6,
  leverage: pos.leverage,
  accountValueUsd: Number(margin.accountValue) / 1e6,
});

Run it against any address with an open BTC position. This run used one of the HLP vault's sub-accounts:

Bash
node 04-precompiles.ts 0x010461c14e146ac35fe42271bdc1134ee31c703a
Text
{
  hyperCoreBlock: 1207301504n,
  market: 'BTC',
  szDecimals: 5,
  oraclePx: 76795,
  markPx: 76768.9,
  user: '0x010461c14e146ac35fe42271bdc1134ee31c703a',
  positionSize: 0.07069,
  entryNotionalUsd: 5423.078874,
  leverage: 20,
  accountValueUsd: 3026167.3495
}

As a check, the same account queried through HyperCore's Info API (clearinghouseState) a few seconds later returned size 0.07069, entry price 76716.3 (5,423.08 / 0.07069) and account value 3026167.3495, the same as the precompiles.

From a Solidity contract

The same reads in a contract use staticcall. This reader returns a position and computes unrealized PnL against the oracle price:

solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.28;

/// Reads a HyperCore perp position and prices from a HyperEVM contract.
/// Precompile addresses and struct layouts follow Hyperliquid's L1Read.sol.
contract PerpPositionReader {
    address constant POSITION = 0x0000000000000000000000000000000000000800;
    address constant ORACLE_PX = 0x0000000000000000000000000000000000000807;

    struct Position {
        int64 szi;
        uint64 entryNtl;
        int64 isolatedRawUsd;
        uint32 leverage;
        bool isIsolated;
    }

    function position(address user, uint16 perp) public view returns (Position memory) {
        (bool ok, bytes memory out) = POSITION.staticcall(abi.encode(user, perp));
        require(ok, "position precompile failed");
        return abi.decode(out, (Position));
    }

    function oraclePx(uint32 perp) public view returns (uint64) {
        (bool ok, bytes memory out) = ORACLE_PX.staticcall(abi.encode(perp));
        require(ok, "oraclePx precompile failed");
        return abi.decode(out, (uint64));
    }

    /// Unrealized PnL in USDC with 6 decimals (HyperCore's USD unit).
    /// Oracle price is scaled by 10^(6 - szDecimals) and szi by 10^szDecimals,
    /// so szi * px is already in 10^6 USD units.
    function unrealizedPnl(address user, uint16 perp) external view returns (int256) {
        Position memory p = position(user, perp);
        if (p.szi == 0) return 0;
        int256 markNtl = int256(p.szi) * int256(uint256(oraclePx(perp)));
        int256 entryNtl = int256(uint256(p.entryNtl));
        return p.szi > 0 ? markNtl - entryNtl : markNtl + entryNtl;
    }
}

The scaling cancels out. szi is in units of 10^szDecimals and the oracle price in units of 10^(6 - szDecimals), so their product is in HyperCore's 6-decimal USD unit, the same unit as entryNtl.

You can run the contract against live HyperCore state without deploying it. A state override on eth_call puts the compiled runtime bytecode at any address for the duration of the call:

TypeScript
// 04b-simulate-contract.ts - run PerpPositionReader against live state, no deployment
import { readFileSync } from 'node:fs';
import { type Hex } from 'viem';
import { client } from './client.ts';

// contracts/PerpPositionReader.json: { abi, deployedBytecode: '0x...' } (solc's evm.deployedBytecode.object, 0x-prefixed)
const { abi, deployedBytecode } = JSON.parse(readFileSync('contracts/PerpPositionReader.json', 'utf8'));
const reader = '0x000000000000000000000000000000000000dEaD' as const; // any unused address
const user = process.argv[2] as Hex;

const stateOverride = [{ address: reader, code: deployedBytecode as Hex }];

const [pos, pnl] = await Promise.all([
  client.readContract({ address: reader, abi, functionName: 'position', args: [user, 0], stateOverride }),
  client.readContract({ address: reader, abi, functionName: 'unrealizedPnl', args: [user, 0], stateOverride }),
]);

console.log({ user, position: pos, unrealizedPnlUsd: Number(pnl) / 1e6 });
Text
{
  user: '0x010461c14e146ac35fe42271bdc1134ee31c703a',
  position: {
    szi: 7069n,
    entryNtl: 5423078874n,
    isolatedRawUsd: 0n,
    leverage: 20,
    isIsolated: false
  },
  unrealizedPnlUsd: 5.354675
}

A short position on a second sub-account (0x31ca...974b, size -0.07325 entered at $76,669.40) returned -$8.98 at the same oracle price, as expected for a short with the price above entry.

Call precompiles at latest only

Precompile reads work only through eth_call (and eth_estimateGas) at the latest block tag. Every other form failed on Dwellir's endpoint:

CallResult
eth_call at latestWorks
eth_call at the current head's block numberPrecompileOutOfGas(50000000)
eth_call at head minus 1, minus 10, or block 50,000,000PrecompileOutOfGas(50000000)
eth_call at pending, safe or finalizedPrecompileOutOfGas(50000000)
debug_traceCall at latestout of gas, all gas consumed
eth_simulateV1 at latestPrecompile call fails, outer call reverts

The reason is in Hyperliquid's JSON-RPC docs: read precompile results "are only recorded for the calls actually made on each block". A node replaying an old block has the HyperCore answers that real transactions asked for and nothing else. A hypothetical read at a past block needs a full HyperCore replay. viem and ethers use latest by default, so this only bites when you pin a block number for consistency across several reads. For a consistent snapshot, batch the reads into one contract call (as unrealizedPnl does), or record l1BlockNumber alongside each read.

For off-chain apps that only need HyperCore data, the precompiles aren't the only route. The Info endpoint serves the same account state as JSON without any EVM encoding.

Write to HyperCore with CoreWriter

CoreWriter is a system contract at 0x3333333333333333333333333333333333333333 with one function, sendRawAction(bytes). It burns about 25,000 gas on purpose before it emits RawAction(address indexed user, bytes data). HyperCore reads that log after the EVM block and executes the action on behalf of msg.sender. The payload is:

  • Byte 1: encoding version, currently 1
  • Bytes 2 to 4: action ID, big-endian
  • The rest: the plain ABI encoding of the action's fields

The action IDs from Hyperliquid's CoreWriter docs:

IDActionFields
1Limit order(uint32 asset, bool isBuy, uint64 limitPx, uint64 sz, bool reduceOnly, uint8 tif, uint128 cloid)
2Vault transfer(address vault, bool isDeposit, uint64 usd)
3Token delegate(address validator, uint64 wei, bool isUndelegate)
4Staking deposituint64 wei
5Staking withdrawuint64 wei
6Spot send(address destination, uint64 token, uint64 wei)
7USD class transfer(uint64 ntl, bool toPerp)
8Finalize EVM contract(uint64 token, uint8 variant, uint64 createNonce)
9Add API wallet(address wallet, string name)
10Cancel order by oid(uint32 asset, uint64 oid)
11Cancel order by cloid(uint32 asset, uint128 cloid)
12Approve builder fee(uint64 maxFeeRate, address builder)
13Send asset(address destination, address subAccount, uint32 sourceDex, uint32 destinationDex, uint64 token, uint64 wei)
15Borrow/lend operation(uint8 operation, uint64 token, uint64 wei)
16Set abstraction(address user, uint8 abstraction)
17Outcome operation(uint8 operation, uint32 question, uint32 outcome, uint64 wei)

For limit orders, tif is 1 for ALO, 2 for GTC and 3 for IOC. A cloid of 0 means no client order ID. limitPx and sz are 10^8 times the human-readable value for every market. Reads use a different scale: a price from the precompile can't go straight into an order.

Encode and simulate an action

05-corewriter.ts encodes a BTC limit order, checks that the sender exists on HyperCore, and simulates the call. eth_call and eth_estimateGas broadcast nothing:

TypeScript
// 05-corewriter.ts - encode a CoreWriter action, pre-flight it, and simulate it (read-only)
import {
  encodeAbiParameters, encodeFunctionData, decodeAbiParameters,
  concatHex, toHex, parseAbi, type Hex,
} from 'viem';
import { client } from './client.ts';

const CORE_WRITER = '0x3333333333333333333333333333333333333333';
const CORE_USER_EXISTS = '0x0000000000000000000000000000000000000810';
const coreWriterAbi = parseAbi(['function sendRawAction(bytes data)']);

// Action 1, limit order. limitPx and sz are 10^8 * the human-readable value.
function encodeLimitOrder(o: {
  asset: number; isBuy: boolean; limitPx: number; sz: number;
  reduceOnly: boolean; tif: 1 | 2 | 3; cloid: bigint;
}): Hex {
  const body = encodeAbiParameters(
    [{ type: 'uint32' }, { type: 'bool' }, { type: 'uint64' }, { type: 'uint64' },
     { type: 'bool' }, { type: 'uint8' }, { type: 'uint128' }],
    [o.asset, o.isBuy, BigInt(Math.round(o.limitPx * 1e8)), BigInt(Math.round(o.sz * 1e8)),
     o.reduceOnly, o.tif, o.cloid],
  );
  const header = concatHex([toHex(1, { size: 1 }), toHex(1, { size: 3 })]); // version 1, action ID 1
  return concatHex([header, body]);
}

const sender = process.argv[2] as Hex;

// Pre-flight: the sender must already exist on HyperCore, or the action is dropped.
const { data: existsRaw } = await client.call({
  to: CORE_USER_EXISTS,
  data: encodeAbiParameters([{ type: 'address' }], [sender]),
  blockTag: 'latest',
});
const [exists] = decodeAbiParameters([{ type: 'bool' }], existsRaw!);

const action = encodeLimitOrder({
  asset: 0, isBuy: true, limitPx: 70000, sz: 0.001, reduceOnly: false, tif: 1, cloid: 0n,
});
const data = encodeFunctionData({ abi: coreWriterAbi, functionName: 'sendRawAction', args: [action] });

await client.call({ account: sender, to: CORE_WRITER, data });
const gas = await client.estimateGas({ account: sender, to: CORE_WRITER, data });

console.log({ sender, coreUserExists: exists, actionHeader: action.slice(0, 10), estimatedGas: gas });

Output for two senders, one with a HyperCore account and one without:

Text
{
  sender: '0x010461c14e146ac35fe42271bdc1134ee31c703a',
  coreUserExists: true,
  actionHeader: '0x01000001',
  estimatedGas: 49910n
}
{
  sender: '0x00000000000000000000000000000000deadbe01',
  coreUserExists: false,
  actionHeader: '0x01000001',
  estimatedGas: 49910n
}

Both simulations succeeded and both cost 49,910 gas. Hyperliquid's docs put a basic call, including the 25,000-gas burn, at about 47,000. The EVM can't tell whether HyperCore will accept the action. The second sender's order would be mined with status 1 and then dropped.

Guard the action in Solidity

In a contract, msg.sender for CoreWriter is the contract, so the contract needs its own HyperCore account and its own balances. This sender reverts on the EVM side when that account is missing, so the failure is visible:

solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.28;

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

/// Places a HyperCore limit order on behalf of this contract's own HyperCore account.
contract OrderSender {
    ICoreWriter constant CORE_WRITER = ICoreWriter(0x3333333333333333333333333333333333333333);
    address constant CORE_USER_EXISTS = 0x0000000000000000000000000000000000000810;
    uint8 constant TIF_IOC = 3;

    /// limitPx and sz are 10^8 * the human-readable value.
    function placeLimitOrder(uint32 asset, bool isBuy, uint64 limitPx, uint64 sz, uint128 cloid) external {
        // CoreWriter acts for msg.sender, which is this contract. Without a HyperCore
        // account the EVM call still succeeds but HyperCore drops the action.
        (bool ok, bytes memory out) = CORE_USER_EXISTS.staticcall(abi.encode(address(this)));
        require(ok && abi.decode(out, (bool)), "contract has no HyperCore account");

        bytes memory action = abi.encodePacked(
            uint8(1),   // encoding version
            uint24(1),  // action ID 1: limit order
            abi.encode(asset, isBuy, limitPx, sz, false, TIF_IOC, cloid)
        );
        CORE_WRITER.sendRawAction(action);
    }
}

placeLimitOrder(0, true, 7_000_000_000_000, 100_000, 0) (buy 0.001 BTC at $70,000) was simulated with simulateContract. A state override placed the OrderSender bytecode at each of two addresses, one with a HyperCore account and one without, so the contract itself was the CoreWriter sender:

Text
0x010461c14e146ac35fe42271bdc1134ee31c703a -> simulation ok, CoreWriter would emit the action
0x00000000000000000000000000000000deadbe01 -> reverted: contract has no HyperCore account

With the guard removed, the RawAction payload the contract emitted matched encodeLimitOrder from 05-corewriter.ts, run with the same arguments and tif: 3, byte for byte.

The guard has a limit. Hyperliquid's interaction timing docs say the account must exist before the EVM block is built. Within one HyperEVM block the order is: EVM block, then EVM-to-Core transfers, then CoreWriter actions. Even though the transfers run first, HyperCore checks account existence against the state from before the EVM block, so an account created by a transfer in the same block doesn't count and that block's CoreWriter action is rejected.

Why a CoreWriter action doesn't land

hl-node issues #171 and #172 describe testnet CoreWriter orders and transfers that succeeded on the EVM, emitted a correct RawAction, and produced no fill, no ledger entry and no error. Neither issue has a maintainer reply. Whatever the cause in those cases, the EVM receipt can't confirm that HyperCore executed anything. Before you assume a bug, check these:

  1. The sender has no HyperCore account. Check coreUserExists (0x...0810) for the address that calls CoreWriter. When a contract calls CoreWriter, that address is the contract.
  2. Wrong scaling. Orders use 10^8 * value for both price and size. Prices must also follow HyperCore's tick and lot rules: at most 5 significant figures and at most 6 - szDecimals decimals for perps (8 - szDecimals for spot), with sizes rounded to szDecimals.
  3. Wrong asset ID. Perps use the index from meta, so BTC is 0. Spot uses 10000 + spot index, so mainnet HYPE/USDC (spot ID 107) is 10107. Builder-deployed perps use 100000 + perp_dex_index * 10000 + index. Token IDs and spot IDs are different numbers.
  4. Checking too early. Order actions and vault transfers from CoreWriter are delayed on-chain by a few seconds, and appear twice in the L1 explorer: once when queued, once when executed.
  5. Missing balance or margin on HyperCore. The EVM balance doesn't count. The contract's HyperCore account has to hold the funds.

To confirm an order, set a non-zero cloid and look it up through the Info endpoint's orderStatus, or read the position precompile in a later block.

CoreWriter sees steady use on mainnet. Over the 50,000 blocks before 06:39 UTC on 3 November 2026 (about 14 hours), 1,454 senders emitted 17,188 RawAction logs: 13,380 sendAsset, 3,380 spotSend and 232 limit orders. Seven used action ID 14, which the docs don't list. The indexer section below produces these counts.

Debug and trace on HyperEVM

The official RPC returns -32601 Method not found for both debug_traceTransaction and trace_block. Dwellir's endpoint serves the full debug_* and trace_* namespaces on paid plans. Each call counts as one response, like any other method. Results from 3 November 2026:

MethodRecent blockBlock 1,000,000 (Mar 2025)
debug_traceTransaction with callTracerYesYes
debug_traceTransaction with prestateTracerYesYes
debug_traceTransaction, default struct loggerYes, 8.5 MB for one swapNot tested
debug_traceBlockByNumberYesYes
trace_transaction, trace_replayTransactionYesYes
trace_blockYesNot tested
trace_filterYes, 100-block maximum rangeNot tested

This script prints the call tree of a transaction and labels HyperCore touchpoints:

TypeScript
// 06-trace.ts - trace a HyperEVM transaction with callTracer and list its internal calls
import { type Hex } from 'viem';
import { client } from './client.ts';

type Call = { type: string; from: Hex; to?: Hex; input: Hex; error?: string; calls?: Call[] };

const hash = process.argv[2] as Hex;
if (!hash) throw new Error('usage: node 06-trace.ts <txHash>');

const trace = (await client.request({
  method: 'debug_traceTransaction' as any,
  params: [hash, { tracer: 'callTracer' }] as any,
})) as Call;

function walk(c: Call, depth = 0) {
  const to = c.to?.toLowerCase() ?? '';
  const tag = to === '0x3333333333333333333333333333333333333333' ? ' [CoreWriter]'
    : to.startsWith('0x00000000000000000000000000000000000008') ? ' [read precompile]'
    : to === '0x2222222222222222222222222222222222222222' ? ' [HYPE system address]' : '';
  console.log(`${'  '.repeat(depth)}${c.type} ${c.to}${tag}${c.error ? ` ERROR: ${c.error}` : ''}`);
  for (const sub of c.calls ?? []) walk(sub, depth + 1);
}
walk(trace);
Bash
node 06-trace.ts 0xca54b36c5b73863c909b45614d835269ee2a9d9824f76e4345306a205c35a144
Text
CALL 0xbf8e46b41751874f5495c27fd431c34f7330de5f
  STATICCALL 0xb88339cb7199b77e23db6e890353e22632ba630f
    DELEGATECALL 0x003f73f58ca78880da3642c5cb71d7459b3fe4f8
  STATICCALL 0x0000000000000000000000000000000000000810 [read precompile]
  STATICCALL 0x0000000000000000000000000000000000000801 [read precompile]
  STATICCALL 0x000000000000000000000000000000000000080f [read precompile]

This contract checks coreUserExists, a spot balance and the account margin summary before it acts. That's the same pre-flight pattern as OrderSender above.

Tracing on HyperEVM differs from Ethereum in two ways:

  • Parity-style traces leave out precompile calls. For the transaction above, trace_transaction returned 3 entries and none of them pointed at a precompile, while callTracer showed all three precompile calls. Across blocks 50,255,566 to 50,255,665, callTracer found 340 precompile calls in 85 of the 100 blocks, and trace_filter with toAddress set to the same precompile addresses found none. To audit HyperCore reads, use debug_trace* with callTracer.
  • Historical traces replay the recorded precompile results. Blocks 30,000,014, 30,000,047 and 30,000,048 (March 2026) contain 25 transactions with 14 precompile calls between them. For every one of those transactions, the trace's success or failure and gas used matched the original receipts. debug_traceCall is the exception: it simulates a new call, which has no recorded result, so precompile calls run out of gas.

The default struct logger returned 8.5 MB and took 3 seconds for one swap. Use callTracer unless you need opcode-level detail.

Running an indexer

You have four ways to get HyperEVM data into your own database:

SourceWhat you getTrade-offs
JSON-RPC (eth_getBlockByNumber, eth_getLogs, receipts)Standard EVM dataPublic RPC: no history, 1,000-block and 1 MiB caps, no system transactions in blocks. Archive RPC: plan limits apply
WebSocket eth_subscribeLive heads and logsNot on the public RPC. Dwellir delivered 76 newHeads in 75 seconds, big block included
S3 s3://hl-mainnet-evm-blocks/Raw blocks and receipts, MessagePack + LZ4, one file per block (0/6000/6123.rmp.lz4)Requester pays transfer. Needs a decoder (example indexer)
Your own non-validating node~/hl/data/evm_block_and_receipts, optional local EVM RPC (--serve-eth-rpc)16 vCPUs, 128 GB RAM, Tokyo, and node operations

For most teams the practical setup is an archive RPC for backfill, plus WebSocket or polling at the head. Here's the indexer that produced the CoreWriter numbers above, built on getLogsChunked:

TypeScript
// 07-index-corewriter.ts - count CoreWriter actions by type over a block range
import { decodeEventLog, parseAbi, hexToNumber, slice, type Hex } from 'viem';
import { client } from './client.ts';
import { getLogsChunked } from './getLogsChunked.ts';

const CORE_WRITER = '0x3333333333333333333333333333333333333333';
const abi = parseAbi(['event RawAction(address indexed user, bytes data)']);
const ACTIONS: Record<number, string> = {
  1: 'limitOrder', 2: 'vaultTransfer', 3: 'tokenDelegate', 4: 'stakingDeposit',
  5: 'stakingWithdraw', 6: 'spotSend', 7: 'usdClassTransfer', 8: 'finalizeEvmContract',
  9: 'addApiWallet', 10: 'cancelByOid', 11: 'cancelByCloid', 12: 'approveBuilderFee',
  13: 'sendAsset', 15: 'borrowLend', 16: 'setAbstraction', 17: 'outcomeOperation',
};

const blocks = BigInt(process.argv[2] ?? '50000');
const head = await client.getBlockNumber();
const t0 = Date.now();
const logs = await getLogsChunked({ address: CORE_WRITER }, head - blocks + 1n, head);

const counts = new Map<string, number>();
const senders = new Set<string>();
for (const log of logs) {
  const { args } = decodeEventLog({ abi, data: log.data, topics: log.topics });
  const data = args.data as Hex;
  const version = hexToNumber(slice(data, 0, 1));
  const id = hexToNumber(slice(data, 1, 4));
  const name = version === 1 ? (ACTIONS[id] ?? `unknown(${id})`) : `version${version}`;
  counts.set(name, (counts.get(name) ?? 0) + 1);
  senders.add(args.user as string);
}

console.log(`${logs.length} RawAction logs from ${senders.size} senders in blocks ${head - blocks + 1n}-${head} (${Date.now() - t0} ms)`);
console.table([...counts].sort((a, b) => b[1] - a[1]).map(([action, n]) => ({ action, n })));
Text
17188 RawAction logs from 1454 senders in blocks 50204943-50254942 (7828 ms)
┌─────────┬─────────────────────┬───────┐
│ (index) │ action              │ n     │
├─────────┼─────────────────────┼───────┤
│ 0       │ 'sendAsset'         │ 13380 │
│ 1       │ 'spotSend'          │ 3380  │
│ 2       │ 'limitOrder'        │ 232   │
│ 3       │ 'usdClassTransfer'  │ 86    │
│ 4       │ 'addApiWallet'      │ 36    │
│ 5       │ 'tokenDelegate'     │ 30    │
│ 6       │ 'setAbstraction'    │ 17    │
│ 7       │ 'approveBuilderFee' │ 8     │
│ 8       │ 'vaultTransfer'     │ 8     │
│ 9       │ 'unknown(14)'       │ 7     │
│ 10      │ 'stakingWithdraw'   │ 2     │
│ 11      │ 'stakingDeposit'    │ 1     │
│ 12      │ 'borrowLend'        │ 1     │
└─────────┴─────────────────────┴───────┘

Five 10,000-block requests covered 14 hours of chain history in under 8 seconds. On the public RPC the same scan needs at least 50 requests at 1,000 blocks each. Pointed at the public RPC with 1,000-block windows, getLogsChunked took 213 seconds to fetch WHYPE logs for just 3,000 blocks, most of it spent waiting out rate limited errors.

Production checklist

  • Key rows on block number. Big and small blocks can share a timestamp.
  • Decide what to do with system transactions. On Dwellir they appear in blocks at index 0 with gas price 0 and placeholder signature fields (r = 0x1, s = the system address). Their hashes therefore differ from the hashes the official eth_getSystemTxsByBlockNumber returns. If you join with data from the public RPC, match system transactions on block, sender, recipient and input, not on hash.
  • Store l1BlockNumber with any precompile-derived value. You can't re-read a precompile at a past block later.
  • Don't trust EVM success for CoreWriter. Store the RawAction log, then reconcile against HyperCore through the Info endpoint or a later precompile read.
  • No reorg rollback needed. HyperBFT commits each block as final, so a block number does not change hash once you've seen it.
  • Keep keys out of code. Read DWELLIR_API_KEY from the environment, as client.ts does, and use a separate key per service so usage is attributable.

If your indexer outgrows shared limits, a Dedicated Node Cluster runs on unmetered Dwellir backend nodes with HyperEVM, Info and gRPC on the same machines. For the full map of Hyperliquid data services and where HyperEVM fits, see the Hyperliquid infrastructure stack and the architecture docs. To decode HyperEVM transactions that arrive inside HyperCore block data, see the evmRawTx reference.

Frequently asked questions

What is the HyperEVM chain ID and RPC URL?

HyperEVM mainnet is chain ID 999 (0x3e7) and testnet is 998. The official mainnet RPC is https://rpc.hyperliquid.xyz/evm, HTTP only, with no WebSocket and no historical state. Dwellir's endpoint is https://api-hyperliquid-mainnet-evm.n.dwellir.com/YOUR_API_KEY over HTTPS and WSS, with archive state from block 1 and the debug and trace namespaces. Endpoint details are on the Hyperliquid network page.

What is the eth_getLogs block range limit on HyperEVM?

Hyperliquid's docs say 50 blocks for the public RPC. On 3 November 2026 the public RPC accepted 1,000 blocks, rejected 2,000 with query exceeds max block range 1000, and rejected any response over 1 MiB. Dwellir allows 500 blocks per request on the Developer plan and 10,000 on Growth and above, with a 20,000-log cap per response.

How do I read HyperCore state like perp positions from a HyperEVM contract?

Call the read precompiles at 0x0000000000000000000000000000000000000800 and up with staticcall. The position precompile at 0x...0800 takes abi.encode(user, perpIndex) and returns size, entry notional, isolated margin, leverage and margin mode. The values match HyperCore state when the EVM block is built. From off-chain code, call the precompiles with eth_call at the latest block tag. Calls at a block number fail.

Why did my CoreWriter action succeed on HyperEVM but do nothing on HyperCore?

CoreWriter only emits a log. HyperCore processes it after the EVM block and drops invalid actions without an EVM revert. The usual causes are a sender with no HyperCore account (the contract itself is the sender), wrong scaling (limitPx and sz are 10^8 times the human value), a price or size that breaks tick and lot rules, or the wrong asset ID (spot is 10000 plus the spot index).

What are big blocks on HyperEVM?

HyperEVM produces a small block every second with a 3M gas limit and a big block every minute with a 30M gas limit, in one shared block number sequence. In a 300-block sample on 3 November 2026, 5 blocks were big. Transactions go to small blocks unless the sender's HyperCore account sets usingBigBlocks with the evmUserModify action.

Does HyperEVM support debug_traceTransaction and trace_block?

The official RPC returns Method not found for both. Dwellir's endpoint serves debug_traceTransaction, debug_traceBlockByNumber, debug_traceCall, trace_transaction, trace_block, trace_replayTransaction and trace_filter on paid plans. debug_traceTransaction, debug_traceBlockByNumber, trace_transaction and trace_replayTransaction also worked on a March 2025 block. debug_traceCall cannot execute read precompiles. Parity-style trace_* output leaves out precompile calls, so use debug_traceTransaction with callTracer when you need to see them.

Which HyperEVM RPC provider is best?

It depends on the workload. The public RPC is enough for a wallet or a light dApp. Indexers need archive state, wide eth_getLogs ranges and WebSocket, and debugging needs debug and trace, which the public RPC lacks. Dwellir serves archive state, WebSocket, debug and trace at 1 API credit per response from $49 a month, with 10,000-block eth_getLogs ranges on Growth and above. The Hyperliquid RPC provider comparison ranks 5 providers on price, archive and HyperCore coverage.

Next steps

  • Extend 07-index-corewriter.ts to decode limit-order fields and join each order against orderStatus by cloid, so you can measure how many CoreWriter orders actually fill.
  • Combine 04-precompiles.ts with a WebSocket newHeads subscription to log HyperCore positions next to the HyperEVM block that read them.
  • Read the method references for eth_getLogs, eth_call and debug_traceTransaction on HyperEVM.

Every result in this guide came from Dwellir's HyperEVM endpoint, with the public RPC for comparison. To run the samples yourself, create a free Dwellir account and move to the Developer plan for eth_getLogs, debug and trace.

Start free.1 response = 1 credit.

100,000 responses a day at 20 per second, on 150+ networks. Upgrade when you need more.