The Hyperliquid /info endpoint is an unauthenticated read POST that exposes order and fill state without a private key. Three read requests cover different authorities: userFills is the execution history, historicalOrders is the account's order history with status, and frontendOpenOrders lists currently resting orders. orderStatus(oid|cloid) is a point lookup that returns a discriminated union (order, status, fill, rejected, unknownOid) and is the authority for a single order. Because userFills and historicalOrders can disagree momentarily due to indexing lag, reconcile them by joining on oid and treating orderStatus as the per-order source of truth. This article shows the request shapes, a runnable Node.js read path, a results table to fill against your own account, and the failure modes that break integrations.
The read-only /info surface and why order observation needs no private key
Hyperliquid splits its HTTP API into two surfaces: /info for reads and /exchange for signed writes. The /info endpoint is an unauthenticated POST that accepts a JSON body with a type field and request-specific parameters, and it returns the requested state. Because it is read-only, observing orders and fills never requires a private key, which means monitoring can run in a separate process, a dashboard, or a read-only service account without exposing signing material.
This differs from JSON-RPC provider endpoints, which are also POST-based but follow the JSON-RPC 2.0 envelope with jsonrpc, method, params, and id fields. The /info surface is a plain application-level POST, not a JSON-RPC method call, so you should not wrap its body in a JSON-RPC envelope. The Hyperliquid API documentation is authoritative for the exact request and response shapes.
For production reads, point your client at a reliable endpoint. OnFinality provides Hyperliquid RPC endpoints and an API service that can front the /info surface; the Hyperliquid network page lists the available access paths.
- /info: unauthenticated read POST, body is a JSON object with a type field.
- /exchange: signed write path for order submission and cancellation.
- No private key is needed to read userFills, historicalOrders, frontendOpenOrders, or orderStatus.
- Do not wrap /info bodies in a JSON-RPC 2.0 envelope; it is not a JSON-RPC method.
The three read requests and what each is authoritative for
userFills returns execution history for an account. Each fill carries px, sz, side, time, fee, closedPnl, oid, and tid, and the request accepts an optional aggregateByTime flag. This is the authority for aggregate execution: if you want to know how much was actually traded and at what prices, userFills is the source. The Hyperliquid API documentation defines the exact field set and the aggregation semantics.
historicalOrders returns the account's order history with status, which is the authority for order-level state transitions. frontendOpenOrders returns currently resting orders, which is the authority for what is live right now. orderStatus(oid|cloid) is a point lookup for a single order and is the authority for that one order's current state. The Hyperliquid Python SDK shows the reference client-side calls for each of these.
A practical read path calls frontendOpenOrders for the live book, historicalOrders for the recent order log, and userFills for executions, then joins them on oid. The Hyperliquid historical data and market data APIs page covers the market-data side, which is separate from your own order state.
- userFills: execution history; authoritative for aggregate fills and fees.
- historicalOrders: order history with status; authoritative for order-level transitions.
- frontendOpenOrders: currently resting orders; authoritative for live exposure.
- orderStatus(oid|cloid): point lookup; authoritative for a single order.
The orderStatus discriminated union and safe branching
orderStatus is keyed by oid or cloid and returns a discriminated union. The documented outcomes are order, status, fill, rejected, and unknownOid. Each outcome has a different shape, so you must branch on the discriminator before reading fields. Treating the response as a single flat object is the single most common integration mistake.
The order outcome describes a resting order, status describes a state transition, fill describes an execution, rejected describes an order that was refused, and unknownOid means the identifier was not found. When you generate your own client order ids, the cloid path lets you look up an order before you have an oid, which is useful for idempotent submission flows. The Hyperliquid documentation is authoritative for how an order transitions from submission to fill and why orderStatus is keyed by oid or cloid.
Branching safely means checking the discriminator first, then validating that the fields you need exist for that branch. Never assume a fill field is present on an order outcome, and never assume an oid exists on an unknownOid outcome.
- order: resting order details.
- status: state transition details.
- fill: execution details.
- rejected: order refused; read the reason.
- unknownOid: identifier not found; check cloid generation and submission.
Why userFills and historicalOrders can disagree momentarily
userFills and historicalOrders are served from different read paths and can disagree for a short window due to indexing lag. A fill may appear in userFills before the corresponding order status updates in historicalOrders, or vice versa. This is normal and does not indicate data loss.
The reconciliation rule is to treat orderStatus as the authority for a single order and userFills as the authority for aggregate execution. When the two disagree, re-query orderStatus for the specific oid and use that result to resolve the order's state. For aggregate volume, fees, and closedPnl, trust userFills. The Hyperliquid API error handling page covers rejection decoding at submission, which is a different phase from this post-submission read path.
If you need a consistent snapshot, poll both surfaces and join on oid, then apply a short retry window before alerting. Do not treat a transient mismatch as a failure.
- Indexing lag can cause temporary disagreement between userFills and historicalOrders.
- orderStatus is the per-order authority; userFills is the aggregate authority.
- Join on oid and retry before alerting on a mismatch.
A runnable Node.js read path for open orders, history, and fills
The following Node.js example POSTs to the /info endpoint with the correct body shapes, reads open orders, historical orders, and recent user fills, and prints a reconciled per-order table joining oid to fills to status. It uses the built-in fetch available in modern Node.js and does not require a private key.
Replace the endpoint URL with your provider's /info URL and set the user address. The example assumes the response shapes documented by Hyperliquid; if your provider returns a wrapper, adjust the parsing accordingly.
const INFO_URL = 'https://api.hyperliquid.xyz/info';
const USER = '0xYourAccountAddress';
async function info(body) {
const res = await fetch(INFO_URL, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body)
});
if (!res.ok) throw new Error('info HTTP ' + res.status);
return res.json();
}
async function main() {
const open = await info({ type: 'frontendOpenOrders', user: USER });
const history = await info({ type: 'historicalOrders', user: USER });
const fills = await info({ type: 'userFills', user: USER, aggregateByTime: false });
const byOid = new Map();
for (const o of history) {
const oid = o.order && o.order.oid;
if (oid == null) continue;
byOid.set(oid, { oid, status: o.status, fills: [] });
}
for (const f of fills) {
const oid = f.oid;
if (oid == null) continue;
if (!byOid.has(oid)) byOid.set(oid, { oid, status: 'unknown', fills: [] });
byOid.get(oid).fills.push(f);
}
for (const o of open) {
const oid = o.oid;
if (oid == null) continue;
if (!byOid.has(oid)) byOid.set(oid, { oid, status: 'open', fills: [] });
}
console.log('oid | status | fills | filledSz | avgPx');
for (const row of byOid.values()) {
let filledSz = 0;
let notional = 0;
for (const f of row.fills) {
const sz = Number(f.sz);
const px = Number(f.px);
filledSz += sz;
notional += sz * px;
}
const avgPx = filledSz > 0 ? (notional / filledSz).toFixed(4) : '-';
console.log(row.oid + ' | ' + row.status + ' | ' + row.fills.length + ' | ' + filledSz + ' | ' + avgPx);
}
}
main().catch((e) => { console.error(e); process.exit(1); });A point lookup with orderStatus for a single order
When you need the state of one order, orderStatus is cheaper and more precise than scanning history. Pass either oid or cloid in the request body. The response is the discriminated union described earlier, so branch on the discriminator before reading fields.
The following curl example shows the request shape for an oid lookup. Replace the oid with a real value from your account. If you generate your own client order ids, use the cloid form instead.
curl -s -X POST https://api.hyperliquid.xyz/info \
-H 'Content-Type: application/json' \
-d '{"type":"orderStatus","user":"0xYourAccountAddress","oid":123456789}'
# cloid form
curl -s -X POST https://api.hyperliquid.xyz/info \
-H 'Content-Type: application/json' \
-d '{"type":"orderStatus","user":"0xYourAccountAddress","cloid":"0x..."}'Results table to fill against your own account
Use the table below to record what your endpoint returns for a known order. Run the Node.js example, pick one oid, and fill in each column. This verifies that your provider returns the documented shapes and that your reconciliation logic joins correctly.
If a column is empty or unexpected, re-check the request body and the discriminator branch before assuming a provider issue. The Hyperliquid RPC endpoints page lists access options if you need a different endpoint.
- oid: the order identifier you looked up.
- orderStatus discriminator: order, status, fill, rejected, or unknownOid.
- historicalOrders status: the status string returned for that oid.
- userFills count: number of fills joined to that oid.
- filledSz: sum of sz across joined fills.
- avgPx: notional divided by filledSz.
- fee total: sum of fee across joined fills.
- closedPnl total: sum of closedPnl across joined fills.
Failure modes: unknownOid, partial fills, aggregation, and time units
unknownOid appears when a cloid was never accepted or when an oid does not exist. If you generate client order ids, verify that the cloid you query matches the one you submitted, including case and prefix. A cloid that was rejected at submission will not resolve later.
Partial fills look like a smaller order if you only read the latest fill. Always sum sz across all fills joined to the oid, and compare against the original order size. aggregateByTime collapses multiple fills into one row, which is useful for reporting but hides per-fill detail; set it to false when you need execution-level granularity.
Time fields are epoch-based. Confirm whether your provider returns seconds or milliseconds and normalize before comparing. Rate-limit behaviour on the info surface varies by provider; consult the provider's rate-limits documentation and back off on 429 responses. The Hyperliquid funding rate mechanics page shows a similar read-path pattern for a different data type.
- unknownOid: cloid never accepted or oid does not exist.
- Partial fills: sum sz across all fills, do not read only the latest.
- aggregateByTime: collapses fills; set false for execution detail.
- Time units: normalize seconds vs milliseconds before comparing.
- Rate limits: back off on 429; behaviour varies by provider.
Limitations and tradeoffs of the read-only info surface
Because /info is unauthenticated, it cannot reconcile writes. You cannot use it to confirm that a signed action was accepted; that requires the /exchange path and its response. Treat /info as an observation surface, not a write-confirmation surface.
Provider caching can introduce staleness. Some providers cache /info responses for short windows, so a freshly placed order may not appear immediately. Reseller gaps are another tradeoff: a reseller may not expose every /info request type, so verify coverage before building on it. The Hyperliquid clearinghouseState page covers margin and position state, which is a different read path from order state.
For production monitoring, combine /info reads with your own submission logs and a retry window. Do not assume a single read is a consistent snapshot.
- No auth means no write reconciliation; use /exchange for that.
- Provider caching can delay visibility of new orders.
- Reseller coverage of /info request types varies.
- Combine reads with submission logs and a retry window.
Troubleshooting checklist for order and fill reads
When a read looks wrong, work through the checklist in order. First confirm the request body type and parameters match the documented shape. Second, confirm you are branching on the orderStatus discriminator. Third, confirm you are joining fills on oid and summing sz rather than reading a single fill.
If the mismatch persists, compare userFills and historicalOrders for the same oid and re-query orderStatus. A transient disagreement is expected; a persistent one suggests a request or parsing bug. The Hyperliquid WebSocket subscriptions page covers the streaming alternative if polling is too slow for your use case.
Finally, check the endpoint itself. If your provider returns errors or truncated data, switch to a known-good endpoint and re-run the results table.
- Verify request body type and parameters.
- Branch on the orderStatus discriminator.
- Join fills on oid and sum sz.
- Re-query orderStatus to resolve transient mismatches.
- Switch endpoints if errors persist.
Next steps for production order and fill monitoring
Build a small reconciliation service that polls frontendOpenOrders, historicalOrders, and userFills on a schedule, joins on oid, and exposes a per-order view. Add orderStatus lookups for orders that need immediate resolution. Keep the read path separate from the signing path so a monitoring outage cannot affect trading.
For endpoint selection and pricing, see RPC pricing and the OnFinality Learn hub for related guides. If you need lower-latency updates, evaluate the WebSocket path alongside the polling read path.
Document your own results table and retry policy, and revisit it when you change providers. The read path is stable, but provider behaviour around caching and rate limits is not.
- Separate read and signing paths.
- Poll and join on oid; add orderStatus for urgent lookups.
- Review RPC pricing and the Learn hub for related guides.
- Re-validate the results table when changing providers.