A reference to account entities on Hyperliquid (HL): the master wallet, agent/API wallet, sub-accounts, vaults, builder address, collateral modes (manual / Unified Account / legacy dex abstraction, portfolio margin), permissions, and address formats. Facts were verified against SDK @nktkas/hyperliquid 0.27.x and viem; items verified live have a date, and items taken from documentation are marked.
TL;DR
- The agent signs; the master trades and is queried. The agent (API wallet) private key signs exchange actions, but
userin every info request is the address of the account that holds the funds (master or sub-account). The agent address is useless for reading state: it has no positions. - An agent cannot withdraw funds and cannot sign
approveBuilderFee. Use a separate agent for automation; it does not need the master account's private key. User or API Wallet 0x… does not existmeans that the agent is not authorized (Authorize was not clicked, or the key expired or was revoked). The error is not transient: every request fails, so retries are useless. The only remedy is a new API wallet followed by Authorize.extraAgentsshows only NAMED agents. An unnamed agent created throughapproveAgentwithout a name can trade but does not appear in the list. The definitive test is the first signed request (for example,updateLeverage).- Sub-account: orders are signed by an agent of the master account, and the request includes
vaultAddress= the sub-account address. In the SDK this isdefaultVaultAddress. A separate key for every sub-account is unnecessary. Verified live 2026-09. userRole(weight 60) returnsmissing | user | agent | subAccount | vaultandmaster. It is the best preflight request: it catches the common mistake of providing an API wallet address instead of an account address.- The collateral mode (
userState.abstraction) is available only in WSwebData3:disabled(manual),unifiedAccount,dexAbstractionEnabled. RESTuserDexAbstractionreturnsfalseforunifiedAccount, so it does not detect the most common mode. HL may silently migrate an account to Unified Account. - To trade through an agent on a HIP-3 dex (
xyz:), callagentEnableDexAbstraction()once. The responseAbstraction transition not allowedmeans that the account is already unified; treat it as success. - On Unified Account, the perp leg is a reserve (
hold/spotHold) inside spot.usdSendandusdClassTransferare disabled there (Action disabled when unified account is active); usesendAssetinstead. Capital is always calculated as follows:Σ perp accountValue (all dexes) + (spot total − hold). - One bot — one account (or sub-account). Two bots on one address fight over positions, and a separate server or IP does not fix that. Revoking an agent does not close positions, and deleting the key from your own database does not revoke it on HL.
1. Account and address types: summary table
| Entity | What it is | Funds/positions | Can sign | How to trade | userRole.role |
|---|---|---|---|---|---|
| Master account | An EVM address that deposited on HL | Its own | Everything, including withdrawals, approveAgent, approveBuilderFee | With its own key or through an agent | user |
| Agent / API wallet | A separate EVM key approved by the master | None (the address is empty) | Trading actions on behalf of the master: orders, cancellations, leverage, agentEnableDexAbstraction. Cannot withdraw or sign approveBuilderFee | Sign with the agent; user = master | agent (data.user identifies the owner) |
| Sub-account | A separate user under the master | Its own: isolated margin, positions, liquidation, and its own address-based rate limit | Has no key of its own; actions are signed by the master's agent/key | Master's agent + vaultAddress = sub-account address | subAccount (data.master) |
| Vault | An account with depositors | Its own | Through the owner (leader) | vaultAddress = vault address | vault (with master) |
| Builder address | A regular address for which users approved a builder fee | A regular account | Like a regular account | Does not trade for users itself: its address is passed in the builder field of orders | user |
| Unknown address | Has never deposited or traded | — | — | — | missing |
2. Agent / API wallet
2.1. Creation and properties
- In the HL UI, an agent is created under the master account: More → API (page
app.hyperliquid.xyz/API) → Generate API Wallet → specify a name and lifetime (up to 180 days) → Authorize (signed by the master). The private key is shown once. - The private-key format is
0x+ 64 hex characters (32 bytes):/^0x[0-9a-fA-F]{64}$/. - Programmatically, an agent is approved by the
approveAgentexchange action signed by the master. Omitting the name creates an unnamed agent (the snippet and EIP-712 fields appear later in this section). - Agent limit: one unnamed and three named agents per account, plus two more for each sub-account. When several bot instances run on one account, each needs its own API wallet, and this limit caps the number of instances.
- An agent cannot withdraw funds. For automation it is a less privileged key than the master key; do not give the automation the master key.
- If the key accidentally belongs to the account itself (key address == account address), trading will work, but this is unsafe. Warn the user and ask for an API wallet.
- The agent address is derived from the private key (
viem,privateKeyToAccount). The key itself does not reveal the master address, so the account address is always passed as a separate parameter.
import { privateKeyToAccount } from 'viem/accounts';
const agent = privateKeyToAccount(AGENT_PRIVKEY as `0x${string}`);
const agentAddress = agent.address.toLowerCase(); // viem returns checksum form—normalize it
const accountAddress = '0xYOUR_ADDRESS'.toLowerCase(); // ACCOUNT address, not the API wallet
Programmatic approval: approveAgent.
According to the HL documentation and SDK d.ts; not verified live programmatically. Verify before use.
import { ExchangeClient, HttpTransport } from '@nktkas/hyperliquid';
import { generatePrivateKey, privateKeyToAccount } from 'viem/accounts';
const transport = new HttpTransport();
// Signed by the MASTER wallet (not the agent). Prefer a browser wallet over putting the master key in a script.
const exMaster = new ExchangeClient({ wallet: masterWallet, transport });
const agentPk = generatePrivateKey(); // show and save ONCE
const agentAddress = privateKeyToAccount(agentPk).address.toLowerCase();
await exMaster.approveAgent({ agentAddress, agentName: 'bot-1' }); // no name creates an unnamed agent, invisible in extraAgents
- EIP-712 type
HyperliquidTransaction:ApproveAgent: fieldshyperliquidChain,agentAddress,agentName,nonce. The domain isHyperliquidSignTransaction, as forapproveBuilderFee(see sdk-and-api.md §8.2). - According to the documentation, lifetime is encoded as a suffix in the name:
agentName = 'bot-1 valid_until 1790000000000'(ms). Check:extraAgentsshould returnvalidUntilwith this value (§2.4). - According to the documentation, another
approveAgentwith the same name replaces the previous agent with that name (not verified; see §11). - After approval, a signed
updateLeveragetest is mandatory (§2.4): the absence of thedoes not existerror is the only reliable confirmation.
2.2. Revocation and lifetime
- Revoke on
app.hyperliquid.xyz/API. The user can revoke an agent directly. - Revoke does not close open positions. It only stops acceptance of new signed actions. Emergency stop ≠ flat: positions must be closed separately, before revocation or manually through the UI.
- Deleting a local copy of the key does not revoke it on HL. If the key leaked, it remains valid until revoke or expiration of
validUntil. To actually disable access, revoke API wallets in the HL account. - An expired
validUntilproduces the same error as an unauthorized agent (see 2.3).
2.3. “agent not authorized” error
The exchange's exact response form:
{ "status": "err", "response": "User or API Wallet 0x… does not exist." }
The SDK throws ApiRequestError: User or API Wallet 0x… does not exist.
- Causes: the key was generated, but the master did not sign the Authorize/Confirm step; the key expired (
validUntilis in the past); the agent was revoked. - The private key is still valid: viem accepts it and derives an address. But HL does not know that address, and every user request fails—
updateLeverage, placement, cancellation. - The error is not transient. Retries are useless. The only remedy is: new API wallet → mandatory Authorize with the master's signature → replace the key in the bot.
- The address in the message varies by user, so match a stable substring:
const AGENT_NOT_EXISTS = /User or API Wallet .* does not exist/i;
// broader variant for the first signed request during preflight:
const NOT_APPROVED = /does not exist|not approved|unauthorized/i;
- Classify it as “dead key,” not as a rejection of a particular order. Otherwise the bot keeps sending orders while an open position remains unmanaged: this key can no longer cancel or close it, and the wind-down procedure will not work with the key either. Close the position with a new key or manually in the UI.
2.4. Authorization check without a trade: extraAgents
POST https://api.hyperliquid.xyz/info
{ "type": "extraAgents", "user": "0xMASTER_ADDRESS" }
→ [ { "address": "0x…", "name": "…", "validUntil": 1790000000000 } ]
validUntilis in milliseconds.0ornullmeans no expiration.- Weight 20: this is considered a “heavy” info request.
- An agent is live if its
address(compare lowercase) appears in the list and!validUntil || validUntil > Date.now(). useris the master (the owner that approved the agent). For a sub-account, this is the master account, not the sub-account address.- Only named agents are shown. An unnamed agent (
approveAgentwithout a name) can trade but does not appear in the list. Therefore, absence fromextraAgentsis a warning, not a block. The first signed request provides the truth (updateLeverageis convenient because it is idempotent). - Agents created through the UI are named (the UI requires a name) and appear in the list. Verified by a smoke test: a working key gives
authorized: true, while a key without Authorize givesnot_listed.
Classification useful immediately after connecting a key, before the first trade:
| reason | Meaning | Action |
|---|---|---|
ok | Present in the list and not expired | Continue |
not_listed | The key was generated, but Authorize was not signed (or the agent is unnamed) | Ask for Authorize; test with a signed request |
expired | validUntil <= now | Create a new API wallet |
query_failed | Network or 5xx | Do not hard-block; the order path will determine the outcome |
import { InfoClient, HttpTransport } from '@nktkas/hyperliquid';
const info = new InfoClient({ transport: new HttpTransport() });
async function verifyAgent(master: string, agentAddress: string) {
try {
const agents = await info.extraAgents({ user: master as `0x${string}` });
const hit = (Array.isArray(agents) ? agents : [])
.find((a: any) => String(a?.address).toLowerCase() === agentAddress.toLowerCase());
if (!hit) return { authorized: false, reason: 'not_listed' as const }; // or an unnamed agent!
const vu = Number((hit as any).validUntil);
if (vu && vu <= Date.now()) return { authorized: false, reason: 'expired' as const, validUntil: vu };
return { authorized: true, reason: 'ok' as const, validUntil: vu || null };
} catch {
return { authorized: false, reason: 'query_failed' as const };
}
}
2.5. Agent↔account binding as a bot invariant (fail-closed)
The agent key and account address are two independent inputs. If the address contains a typo (or names another account belonging to the same person), the bot reads account B, while every signed order executes on account A, where this agent was approved. A bot reading the wrong account always sees a flat position and repeats entries, while positions accumulate on A without protection: the bot cannot see, protect, or close them.
Rules:
- Check the binding (
extraAgents(account)containsagentAddress) before the first submission. - Fail closed until the first positive response. Trading is disabled until the exchange has confirmed the pair at least once during this process. An unavailable response is not evidence of validity.
- Set the recheck frequency explicitly, accounting for the possibility that API wallets can be revoked after the initial binding.
- A definite mismatch (the exchange responded but the agent is absent from the list) changes the account state to “unmanaged”: trading stops, an explicit reason (which agent and which account) is logged, and an alert is raised.
- Changing the key invalidates the previous check result. Never write a private key to logs.
3. Sub-accounts
- A sub-account is a separate user: its own funds, margin, positions, liquidation, and its own address-based rate limit (a fresh sub-account has a budget of 10,000 requests). Create it in the UI (Sub-Accounts → Create) or through the API.
- Trading: actions are signed by an agent of the master account (or by the master key), and the request adds
vaultAddress= the sub-account address, just as for a vault. The agent must be approved on the master account, andextraAgentschecks also query the master. - State reads are ordinary info requests with
user= the sub-account address (clearinghouseState,spotClearinghouseState,frontendOpenOrders…). - Verified live (2026-09): orders,
updateLeverage, and fills on a sub-account work through the master's agent withvaultAddress. - A sub-account can also be in unified mode (see 5.4).
SDK (@nktkas/hyperliquid, confirmed from the README and SDK code):
| Method | Purpose |
|---|---|
exchange.createSubAccount({ name }) | Create a sub-account; returns its address |
exchange.subAccountTransfer({ subAccountUser, isDeposit, usd }) | Transfer perp USDC between master and sub-account |
exchange.subAccountSpotTransfer(...) | Transfer spot tokens between master and sub-account |
exchange.subAccountModify(...) | Modify a sub-account (name) |
info.subAccounts({ user }) | List the master's sub-accounts |
order / cancel / updateLeverage with vaultAddress | Trade on behalf of the sub-account |
import { ExchangeClient, HttpTransport } from '@nktkas/hyperliquid';
import { privateKeyToAccount } from 'viem/accounts';
const transport = new HttpTransport({ isTestnet: false, timeout: 10_000 });
const agent = privateKeyToAccount(AGENT_PRIVKEY as `0x${string}`); // MASTER account's agent
// Master account: do not pass vaultAddress
const exMain = new ExchangeClient({ wallet: agent, transport });
// Sub-account: every client action will include vaultAddress
const exSub = new ExchangeClient({
wallet: agent,
transport,
defaultVaultAddress: '0xSUBACCOUNT_ADDRESS',
});
Sub-account pitfalls:
reserveRequestWeightdoes not carryvaultAddress. Purchased address-based capacity (0.0005 USDC per request) is credited to the master account or agent account, not the sub-account. Disable purchases when trading through a sub-account.- Master and sub-account must differ (
master !== account). - One master and one agent can serve N sub-accounts: the master's agent signs for any sub-account through
vaultAddress; the API supports this.
4. userRole — “what kind of address is this?” preflight
POST /info { "type": "userRole", "user": "0x…" }
→ { "role": "missing" | "user" | "agent" | "subAccount" | "vault", "data": { "user"?: "0x…", "master"?: "0x…" } }
- IP weight 60. Read it once at startup.
agent→data.useris the account on which this API wallet was approved.subAccount→data.masteris the master account.missingmeans the exchange does not know the address (it has never deposited or traded). Usually this is a typo.
Preflight logic:
type Role = 'missing' | 'user' | 'agent' | 'subAccount' | 'vault';
async function preflight(account: string, configuredMaster?: string) {
const r = await fetch('https://api.hyperliquid.xyz/info', {
method: 'POST', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ type: 'userRole', user: account }),
}).then((x) => x.json()) as { role: Role; data?: { user?: string; master?: string } };
let vaultAddress: string | null = null;
let owner = account; // account on which the agent must be approved
if (r.role === 'agent')
throw new Error(`${account} is an API wallet address, not an account address (account: ${r.data?.user})`);
if (r.role === 'missing')
throw new Error(`${account} is unknown to the exchange—check the address`);
if (r.role === 'subAccount' || r.role === 'vault') {
vaultAddress = account;
owner = (r.data?.master ?? configuredMaster ?? '').toLowerCase();
if (!owner) throw new Error('subaccount/vault, but the master account is unknown');
if (configuredMaster && configuredMaster.toLowerCase() !== owner)
throw new Error('configured master does not match the master reported by the exchange');
}
// then: extraAgents(owner) is a warning; updateLeverage is the definitive test
return { vaultAddress, owner };
}
5. Collateral modes: manual / Unified Account / dex abstraction / portfolio margin
5.1. Modes
userState.abstraction (webData3) | Name | Collateral across dexes | Is agentEnableDexAbstraction required? |
|---|---|---|---|
disabled | manual / isolated dexes (legacy) | Each dex (main, xyz, …) has separate collateral; USDC is transferred separately | Yes, the agent can call it itself |
unifiedAccount | Unified Account (default for new accounts) | Shared automatically across all dexes and spot | No, the agent inherits access. The call returns Abstraction transition not allowed |
dexAbstractionEnabled | legacy dex abstraction (enabled by the master through app.hyperliquid.xyz) | Shared | No, the agent inherits it |
A separate marker is portfolioMarginEnabled: true in spotClearinghouseState (portfolio margin). It changes the semantics of hold; see 5.5.
HL may migrate an account to Unified Account without any action by the owner. If a specific mode matters to the account, the mode must be monitored.
5.2. How to determine the mode
| Source | What it provides | Suitable? |
|---|---|---|
WS webData3 → data.userState.abstraction | Exact string disabled / unifiedAccount / dexAbstractionEnabled | Yes, the only source of the string |
REST {"type":"userDexAbstraction","user":…} (weight 2) | boolean | null: true only for legacy dexAbstractionEnabled | Not for unified: returns false (verified 2026-06-15 on several unified accounts) |
REST webData2 (userState / clearinghouseState) | It has no abstraction, dexAbstractionEnabled, accountType, or isVault keys | No |
Consequence: a mode watchdog based on REST userDexAbstraction stays silent during a manual → unifiedAccount transition.
import WebSocket from 'ws';
function readAbstractionMode(user: string, timeoutMs = 6000): Promise<string | null> {
return new Promise((resolve) => {
let done = false;
const ws = new WebSocket('wss://api.hyperliquid.xyz/ws');
const finish = (v: string | null) => { if (done) return; done = true; try { ws.close(); } catch {} resolve(v); };
ws.on('open', () => ws.send(JSON.stringify({ method: 'subscribe', subscription: { type: 'webData3', user } })));
ws.on('message', (raw) => {
let msg: any; try { msg = JSON.parse(raw.toString()); } catch { return; }
if (msg.channel === 'webData3') {
const a = msg?.data?.userState?.abstraction;
finish(typeof a === 'string' ? a : null); // 'disabled' = manual
}
});
ws.on('error', () => finish(null));
setTimeout(() => finish(null), timeoutMs);
});
}
To monitor the mode, periodically read the string through a short-lived socket and compare it with the previous value. null (timeout) does not count as a mode change.
5.3. agentEnableDexAbstraction — agent access to HIP-3 dexes
- No parameters:
await exchange.agentEnableDexAbstraction(). Available inExchangeClientSDK ≥ 0.27. Signed by the agent, not the master. - Required for an agent to place orders on a HIP-3 / builder dex (
xyz:and similar) for an account in isolated-dex mode. Without it, orders onxyz:are rejected. A successful call moves the account to abstraction mode. It is unnecessary for the main perp dex. - Call it lazily before the first HIP-3 order (or at startup if the configuration contains a market with a dex prefix), once per process. Success can be persisted to avoid sending the call again after a restart.
- Idempotent: safe to retry on 5xx and network errors, like
updateLeverage. Abstraction transition not allowed(regex/transition\s+not\s+allowed/i) means the master is already inunifiedAccountordexAbstractionEnabled. This is success: the agent inherits access and HIP-3 orders will work.- Any other error is real. Do not submit the order; record the enable error in the order status, or HL will reject it with an unclear reason. For closing orders, still configure a retry: the closing path must not contain an unretired failure point.
- Concurrent HIP-3 signals must wait for one shared Promise so that enable is sent exactly once.
let dexAbstractionReady = false;
let pending: Promise<void> | null = null;
async function ensureDexAbstraction(exchange: ExchangeClient): Promise<void> {
if (dexAbstractionReady) return;
if (pending) return pending;
pending = (async () => {
try {
await exchange.agentEnableDexAbstraction();
} catch (err) {
if (!/transition\s+not\s+allowed/i.test(String(err))) throw err; // real error
// already unifiedAccount / dexAbstractionEnabled—access is inherited
}
dexAbstractionReady = true;
})().finally(() => { pending = null; });
return pending;
}
5.4. Unified Account: how funds are represented
- The perp leg is a reserve inside spot. The
hold(orspotHold) field inspotClearinghouseStatemirrors perp margin. Moving funds from perps to spot does not changetotalstablecoins by even a cent; only the free balance (total − hold) changes. - When the perp position closes to 0,
spot totalis bit-for-bit unchanged,holdgoes to 0, free spot increases by the same amount, and capital does not change by a cent. This is not a withdrawal. clearinghouseStatefor a unified account may return zero for a live account: in the observation, it did so for most reads.- HL changes the representation convention on the fly (observed 2026-08-25/26, within hours):
- (a) spot separate: perp
accountValueshows only position margin, and the rest of the capital is in free spot; - (b) spot moved into perps as cross collateral: perp
accountValue≈ all capital, and free spot is near zero. - The result of the capital formula matches the UI under both conventions.
- (a) spot separate: perp
- Invariant capital formula:
equity = Σ perp accountValue (across all dexes) + (spot total − hold). For portfolio margin, usespotHoldinstead ofhold. The formula was verified live under both conventions. - Do not add all spot to perp equity: the reserved part is already included in
accountValue. holdincreases with every placed order, so free spot fluctuates noticeably within a minute. For balance display,totalis more truthful; for sizing, usetotal − hold(the conservative side).- Free spot USDC actually serves as collateral for perps. Verified 2026-09-14 on a unified account: a post-only order was accepted with a zero perp leg and USDC only in spot; no transfer was needed.
- Internal spot ↔ perps transfers (
accountClassTransferin the ledger) on a unified account are neither deposits nor withdrawals. Do not count them as cash flow in PnL and drawdown. - Withdrawal/drain guards that measure only one perp leg produce false positives on a unified account: the perp balance “falls by 100%,” so the guard treats it as a withdrawal or drain even though the funds merely moved to spot. In addition, while
clearinghouseStatereturns zero, such a guard is effectively asleep.
Actions disabled on a unified account:
| Action | Result | Replacement |
|---|---|---|
usdSend | Action disabled when unified account is active | sendAsset |
usdClassTransfer (spot ↔ perp) | Same | Not needed: the pool is shared |
Code that sweeps Spot → Perps through usdClassTransfer must disable itself after catching this error.
// USDC token id comes from spotMeta
const USDC_TOKEN = 'USDC:0x6d1e7cde53ba9467b783cb7c530ce054';
await exchange.sendAsset({
destination: '0xRECIPIENT_ADDRESS',
sourceDex: 'spot', // funds received through HL Send are in the spot pocket
destinationDex: 'spot',
token: USDC_TOKEN,
amount: '10',
});
5.5. Portfolio margin (portfolioMarginEnabled: true)
- All spot USDC serves as collateral for perps. Borrowing capacity is available (fields
borrowed,ltv). USDC can be supplied to Earn (borrow/lend supply): it is then subtracted fromspotHoldbut remains intotal. - HL redefines
hold: it becomes net “reserve − available borrowing capacity” and turns negative. The actual reserve moves to the newspotHoldfield.- Observation 2026-08-29: for USDC,
spotHold == totalbit-for-bit, whileholdwas strongly negative. For a token withtotal = 0,hold ≈ −999999.99,spotHold = 0. spotHoldmirrors perp equity: it matchesΣ perp accountValuewithin fractions of a percent, just asholddoes on an ordinary unified account.
- Observation 2026-08-29: for USDC,
- Conclusion: a PM account has no free spot; all USDC is pledged and already included in
accountValue. - A standard spot parser with the invariants
hold ≥ 0andhold ≤ totalbreaks on these accounts. Use a separate branch:free = total − spotHold. - Part of a PM account's capital may be in Earn and spot bids and can move abruptly for reasons unrelated to trading.
perp accountValue,spot total, andspot freeare three independently changing values. The risk metric must use the one in which trading actually occurs.
5.6. HIP-3 dexes and collateral
xyz:is a separate exchange inside HL (HIP-3): it has its own market list, prices, and margin account.clearinghouseStateandfrontendOpenOrderswithoutdexreturn only the main perp dex. Each builder dex needs a separate request:{"type":"clearinghouseState","user":A,"dex":"xyz"}. An account may look flat on main while holding everything on xyz.- In isolated mode (
disabled), USDC for HIP-3 markets must be transferred to the dex separately. Equity on the main account does not support orders on xyz. Calculate exposure and equity caps per dex. - With abstraction enabled (unified or legacy), main USDC automatically backs HIP-3 orders. From the HIP-3 docs: “For USDC HIP-3 positions, collateral comes from your USDC (Perps) available balance.” Risk is then calculated across one pool:
marginRatio = (main.marginUsed + xyz.marginUsed) / (main.accountValue + xyz.accountValue + freeStables)
- Cross-check: the sum of
accountValueacross main and xyz matchedportfolio.perpDay.accountValue. The only difference was price movement during the requests. - For HIP-3 orders, field
acontains a composite id:100000 + dexIndex * 10000 + assetIndex. See the orders file for details.
6. Vaults
{"type":"vaultDetails","vaultAddress":…}returnsnullfor a regular account. This is how to check whether an address is a vault.userRolefor a vault givesrole: "vault"and its master (owner).- Trading on behalf of a vault uses
vaultAddress, as for a sub-account. {"type":"subAccounts","user":master}returns the master's sub-accounts. This is useful in the same set of “what kind of account is this?” checks.- HLP is structured as a parent with child vaults (read-only, 2026-09-23):
vaultDetailsfor HLP returnsname: "Hyperliquidity Provider (HLP)"andrelationship: { type: "parent", data: { childAddresses: [...] } }—7 addresses. Orders and positions reside on child addresses: the first one had open orders and positions. By response shape (values were not retained):cloidwasnullon every order,tifwas a string on every order, everychildrenarray was empty, and none of theopenOrdersentries had areduceOnlykey—there were no trigger, TP/SL, or reduce-only orders. This is a convenient public source for testing parsers against the response shapes of a “live” account (orders, positions, funding). {"type":"vaultSummaries"}returned[]on mainnet 2026-09-23:/infocannot provide a vault list this way.
7. Builder account and builder fee
- There is no special account type. A builder is an ordinary EVM address for which a user approved a fee through
approveBuilderFee. The user's orders, including those signed by the user's agent, then pass this address in the builder field, and the exchange credits the builder fee. One builder address can serve every user of a service. - Only the user's master wallet can sign
approveBuilderFee; an agent cannot. Approval is therefore a separate step performed by the user in their wallet: the bot cannot do it with the agent key. approveBuilderFeeis authorization (a maximum rate), not a charge.- The currently approved maximum is read with the
maxBuilderFeeinfo request (by master and builder). Approval changes rarely, so the result can be cached by master address. - The collateral mode of a builder address, like that of any account, can change without owner action (see 5.1–5.2). Monitor it if it matters.
8. Address formats
| Item | Format | Regex |
|---|---|---|
| Address (account, agent, sub-account, vault, builder) | 0x + 40 hex characters, 42 characters total, full | lowercase storage: /^0x[0-9a-f]{40}$/; human input (checksum allowed): /^0x[0-9a-fA-F]{40}$/ |
| Agent private key | 0x + 64 hex characters | /^0x[0-9a-fA-F]{64}$/ |
| Spot token id | NAME:0x + 32 hex characters | for example, USDC:0x6d1e7cde53ba9467b783cb7c530ce054 |
- HL accepts any casing for an address in the
userfield, including lowercase. - Normalize to lowercase immediately at ingress: CLI arguments, database reads, WS subscriptions, cache and kv keys, and comparisons with
extraAgents. Mixing case breaks deduplication: one address gets two records and the cache misses. viemprivateKeyToAccount(...).addressreturns checksum form. Convert it to lowercase before comparison.- Logs and reports often abbreviate addresses (first and last characters separated by an ellipsis), but the API requires the full address. An abbreviated address cannot even be used as a key.
- A common input error is putting the API wallet address in the “account address” field.
userRolereturnsagent: produce an understandable error rather than “account is empty.”
9. Architectural rules
- One bot — one account (its own address + its own agent), or at least one sub-account per strategy. Two bots that each reconcile
clearinghouseStateagainst their own target state will close and reduce each other's positions. This is a conflict at the account level: a separate server or IP does not help. - Read the account's current position from
clearinghouseState(szi,positionValue) rather than reconstructing it from the fill feed:userFillsis incomplete (for example, it excludes TWAP slices). - Manual trades on a bot account. A bot that reconciles account positions treats a manual position as its own; keep manual trading on a separate account.
- Stop ≠ flat. Disabling trading and revoking an agent do not close positions. The emergency procedure must explicitly decide what to do with positions.
10. Pitfalls
| What breaks | Why | Correct approach |
|---|---|---|
Every order fails with User or API Wallet 0x… does not exist; a position is stranded | The agent is unauthorized, expired, or revoked. The key is valid locally, but HL does not know it | Classify as “dead key,” do not retry, notify the user, create a new API wallet + Authorize |
| The bot reads an empty account and repeatedly enters | The account address ≠ the account on which the agent was approved: it reads B but executes on A | Fail-closed binding check through extraAgents before trading and periodically |
| Preflight rejects a working agent | extraAgents does not show unnamed agents | extraAgents is only a warning; a signed updateLeverage is authoritative |
| The “account address” field contains an API wallet address; the bot sees an empty account | An agent address holds no positions | userRole → agent → understandable error with the master address from data.user |
Orders on xyz: are rejected | The agent lacks agentEnableDexAbstraction on an isolated account | Call it before the first HIP-3 order |
agentEnableDexAbstraction “fails,” and the bot reports that “HIP-3 will not work” | Abstraction transition not allowed means the account is already unified and orders will work | Match /transition\s+not\s+allowed/i and treat it as success |
| The mode watchdog stays silent during migration to Unified Account | REST userDexAbstraction returns false for unifiedAccount | Read userState.abstraction from WS webData3 |
| A “100% withdrawal/drain” guard fires even though no funds were withdrawn | Unified: the perp leg is a reserve inside spot and moved to spot | equity = Σ perp accountValue + (spot total − hold); measure capital, not one leg |
| Equity is doubled | All spot was added to perps even though hold already mirrors perp margin | Add only total − hold |
The spot parser throws on hold < 0 | Portfolio margin: hold = reserve − borrowing capacity; the real reserve is in spotHold | Branch on portfolioMarginEnabled: free = total − spotHold |
usdSend / usdClassTransfer → Action disabled when unified account is active | Legacy transfers are disabled on a unified account | sendAsset({ sourceDex, destinationDex, token, amount }); disable spot → perp sweep |
| HIP-3 positions are “invisible,” and the account appears flat | clearinghouseState without dex returns only main | Make a separate request with dex: "xyz" for each dex |
| Purchased request capacity did not help the sub-account | reserveRequestWeight without vaultAddress credits weight to the master account | Do not purchase it on a sub-account; use the sub-account's own limit |
| Two bots close each other's positions | Both reconcile one account to their own target | Separate account or sub-account per bot. Different IPs do not help |
| “We deleted the key, so access is disabled” | Deleting it from your own database does not revoke the agent on HL | Revoke on app.hyperliquid.xyz/API |
| “We revoked the agent, so positions are closed” | Revoke only blocks new actions | Close positions before revocation or manually |
| One address creates two records in the cache or database | Checksum and lowercase forms are mixed | Normalize to lowercase at ingress |
/health is green while positions are unmanaged | A cycle without a key or confirmed agent was counted as successful | Cycle success requires a confirmed agent binding wherever execution is mandatory |
11. Open questions / not verified
agentEnableDexAbstractionthroughvaultAddresson a sub-account has not been verified by a smoke test.updateLeveragethroughvaultAddresson a sub-account was verified live 2026-09-14; this later fact closed the earlier “not verified” item.agentEnableDexAbstractionon a fresh isolated (disabled) account has not been verified with a live key; the “transition not allowed” response on an already unified account was observed live. If HIP-3 orders are rejected, begin diagnosis here.- Lifetime of an agent created through the UI. The UI allows up to 180 days, but what happens at expiration—whether the error is the same or different and whether reapproval is needed—has not been observed live.
- Semantics of
validUntil = 0inextraAgents. Both interpretations occur:0as expired and as no expiration. This guide uses “0/null= no expiration” because it is less disruptive; otherwise a non-expiring agent would be falsely markedexpired.0has not been observed live. - Weight of
extraAgents. It is counted both as a light request and as 20 in different sources. This guide uses 20. - Relationship between
portfolioMarginEnabledandabstraction = unifiedAccount. Whether a PM account is always unified and whether unified without PM (hold ≥ 0) exists has not been checked systematically. In practice: branch onportfolioMarginEnabled+ formula withhold/spotHold. - Isolated mode vs shared collateral for HIP-3. “USDC for xyz must be transferred separately” applies to
disabled; “main USDC backs xyz” applies to unified and abstraction. This has not been compared across accounts in different modes. Read the mode before sizing HIP-3. - Units of the
usdfield insubAccountTransferhave not been verified live. Check the documentation and SDK before the first transfer. - Exact error message when the agent limit is exceeded (1 unnamed + 3 named + 2 per sub-account) has not been observed.
- Repeated
approveAgent: whether a new unnamed agent replaces the previous unnamed one and a new agent with the same name replaces the previous named one (as stated in the documentation) has not been verified live. Thevalid_untilsuffix inagentNamehas not been verified either. - Published accrued builder-fee data may lag (not verified; for daily-dump lag, see fees.md §4.3).
clearinghouseStatefor a unified account returns zero in most reads—an observation on one account. It is unknown how general this behavior is. Calculate capital with the formula that includes spot.
Knowledge snapshot: 2026-09; dates of individual checks appear in the text. The HL API changes—recheck limits and response shapes.
© markpaper contributors. Licensed under CC BY 4.0: when publishing or adapting this material, credit “markpaper — Hyperliquid knowledge base” and include links to the original and the license.