A C++ SDK for interacting with the Hyperliquid decentralized exchange, supporting core trading features including order placement, cancellation, position management, and market data queries.
New to this SDK? → See QUICKSTART.md for a quick 2-minute setup guide.
- ✅ Place and cancel limit orders
- ✅ Execute market orders with slippage protection
- ✅ Modify existing orders
- ✅ Bulk order operations
- ✅ Query positions and account state
- ✅ Transfer USD and spot tokens
- ✅ Leverage management
- ✅ Multi-signature accounts
- ✅ WebSocket subscriptions, and WebSocket transport for any request
- ✅ Staking: deposit, withdraw, delegate, plus borrow/lend and prediction-market queries
- ✅ Full EIP-712 signing support
- ✅ Testnet and Mainnet support
macOS:
brew install curl openssl@3 cmakeUbuntu/Debian:
sudo apt-get install -y libcurl4-openssl-dev libssl-dev cmake build-essentialmake buildEasy way - Use the compile script:
./compile.sh your_file.cpp
./your_fileOr add your file to examples/ and use CMake:
# Move your file to examples/
mv your_file.cpp examples/
# Add to examples/CMakeLists.txt, then:
cd build
make your_filemake examples
export HYPERLIQUID_PRIVATE_KEY="0x..."
./build/examples/basic_order- C++17 or later
- CMake 3.15+
- libcurl
- OpenSSL 3.0+
- zlib (websocket builds only, for
fastAssetCtxspayloads) - nlohmann/json (auto-downloaded via CMake)
- msgpack-c (auto-downloaded via CMake)
- IXWebSocket (auto-downloaded via CMake; disable with
-DBUILD_WEBSOCKET=OFF)
sudo apt-get update
sudo apt-get install -y libcurl4-openssl-dev libssl-dev cmake build-essentialbrew install curl openssl@3 cmake# Clone or navigate to the repository
cd hyperliquid-cpp
# Create build directory
mkdir build && cd build
# Configure with CMake
cmake ..
# Build
make -j$(nproc)
# Optional: Install system-wide
sudo make installCreate a file my_bot.cpp:
#include <hyperliquid/exchange.hpp>
#include <hyperliquid/utils/constants.hpp>
#include <iostream>
#include <cstdlib>
int main() {
const char* key = std::getenv("HYPERLIQUID_PRIVATE_KEY");
if (!key) {
std::cerr << "Set HYPERLIQUID_PRIVATE_KEY\n";
return 1;
}
auto wallet = hyperliquid::Wallet::fromPrivateKey(key);
hyperliquid::Exchange exchange(wallet, hyperliquid::TESTNET_API_URL);
// Your trading logic here
auto result = exchange.marketOpen("ETH", true, 0.1);
std::cout << result.dump(2) << "\n";
return 0;
}Compile and run:
./compile.sh my_bot.cpp
export HYPERLIQUID_PRIVATE_KEY="0x..."
./my_bot#include <hyperliquid/exchange.hpp>
#include <hyperliquid/utils/constants.hpp>
int main() {
// Create wallet from private key
auto wallet = hyperliquid::Wallet::fromPrivateKey("0x...");
// Create exchange client (testnet)
hyperliquid::Exchange exchange(wallet, hyperliquid::TESTNET_API_URL);
// Place a limit order
hyperliquid::OrderType order_type;
order_type.limit = hyperliquid::LimitOrderType{"Gtc"};
auto result = exchange.order(
"ETH", // coin
true, // is_buy
0.2, // size
1100.0, // limit_px
order_type,
false // reduce_only
);
return 0;
}Set your private key as an environment variable:
export HYPERLIQUID_PRIVATE_KEY="0x..."Working examples live in the examples/ directory. The main ones:
Place and cancel a limit order:
export HYPERLIQUID_PRIVATE_KEY="0x..."
./build/examples/basic_orderExecute a market order and close the position:
./build/examples/basic_market_orderQuery positions and open orders for any address:
./build/examples/query_positions 0x1234567890abcdef...Place and cancel multiple orders in a single request:
./build/examples/bulk_ordersPlace a TWAP, watch its slice fills, then cancel the remainder. Note this example sleeps 65 seconds to let the first suborders execute:
./build/examples/twap_orderSend ordinary requests over the websocket instead of HTTP. Read-only, no API key:
./build/examples/websocket_post_requestsOther examples cover funding history, historical orders, user fees, candles, account queries, perp deployment queries, and websocket subscriptions.
// Place single order
nlohmann::json order(
const std::string& coin,
bool is_buy,
double sz,
double limit_px,
const OrderType& order_type,
bool reduce_only = false,
const std::optional<Cloid>& cloid = std::nullopt,
const std::optional<BuilderInfo>& builder = std::nullopt
);
// Place multiple orders
nlohmann::json bulkOrders(
const std::vector<OrderRequest>& orders,
const std::optional<BuilderInfo>& builder = std::nullopt,
const std::string& grouping = "na"
);
// Market orders
nlohmann::json marketOpen(const std::string& coin, bool is_buy, double sz,
std::optional<double> px = std::nullopt,
double slippage = 0.05);
nlohmann::json marketClose(const std::string& coin,
std::optional<double> sz = std::nullopt);// Cancel by OID
nlohmann::json cancel(const std::string& coin, int64_t oid);
// Cancel by Client Order ID
// Cancel by client order id. Its own action ("cancelByCloid"), not the plain
// cancel action with a cloid in the oid field.
nlohmann::json cancelByCloid(const std::string& coin, const Cloid& cloid);
// Bulk cancel
nlohmann::json bulkCancel(const std::vector<CancelRequest>& cancels);nlohmann::json modifyOrder(const OidOrCloid& oid,
const std::string& coin,
bool is_buy,
double sz,
double limit_px,
const OrderType& order_type,
bool reduce_only = false);
nlohmann::json bulkModifyOrders(const std::vector<ModifyRequest>& modifies);A TWAP splits one large order into suborders executed every 30 seconds over the requested duration, each with a maximum slippage of 3%.
// Place a TWAP: spread the order over `minutes`
nlohmann::json twapOrder(const std::string& coin,
bool is_buy,
double sz, // total size across the whole TWAP
int minutes, // duration to spread it over
bool reduce_only = false,
bool randomize = false); // randomize suborder timing
// Cancel a running TWAP by its id
nlohmann::json twapCancel(const std::string& coin, int64_t twap_id);sz is rounded to the asset's szDecimals, same as order().
Checking the result is not optional. A rejected TWAP still returns HTTP 200 with
"status": "ok" at the top level — the real outcome lives in response.data.status,
which is either {"running": {"twapId": N}} or {"error": "..."}:
auto result = exchange.twapOrder("ETH", true, 0.1, 30);
auto status = result["response"]["data"]["status"];
if (status.contains("error")) {
std::cerr << "TWAP rejected: " << status["error"].get<std::string>() << "\n";
return 1;
}
int64_t twap_id = status["running"]["twapId"];
// Watch it execute
auto fills = exchange.info_.userTwapSliceFills(address);
// Cancel the remainder. Failure surfaces the same way.
auto cancel_result = exchange.twapCancel("ETH", twap_id);
auto cancel_status = cancel_result["response"]["data"]["status"];
bool ok = cancel_status.is_string() && cancel_status == "success";Valid TWAP durations are enforced by the API, not by the SDK — an out-of-range
duration comes back as "Invalid TWAP duration: N min(s)".
// Transfer USD
nlohmann::json usdTransfer(double amount, const std::string& destination);
// Transfer spot tokens
nlohmann::json spotTransfer(double amount,
const std::string& destination,
const std::string& token);
// Move USDC between your own spot and perp balances
// (to_perp=true: spot -> perp, false: perp -> spot)
nlohmann::json usdClassTransfer(double amount, bool to_perp);
// Transfer a token between dexes ("" = default perp dex, "spot" = spot).
// With a perp dex involved, token must be that dex's collateral token.
nlohmann::json sendAsset(const std::string& destination,
const std::string& source_dex,
const std::string& destination_dex,
const std::string& token,
double amount);// Create a sub-account; the response carries its address.
nlohmann::json createSubAccount(const std::string& name);
// Move perp USDC in (is_deposit=true) or out; usd is in micro-USDC.
nlohmann::json subAccountTransfer(const std::string& sub_account_user,
bool is_deposit,
int64_t usd);
// Spot counterpart; token is "NAME:0x..." as returned by spotMeta().
nlohmann::json subAccountSpotTransfer(const std::string& sub_account_user,
bool is_deposit,
const std::string& token,
double amount);
// Read side: the master account's sub-accounts and their state.
nlohmann::json querySubAccounts(const std::string& user);These act on the master account, so they are signed by the account's own wallet
and ignore any vault/subaccount configured on the Exchange. Same for
setReferrer():
// One shot per account, and only before it has traded.
nlohmann::json setReferrer(const std::string& code);// Approve an agent (API) wallet that can sign orders/cancels for this account
// but cannot transfer funds. Generates the agent key locally and returns it
// alongside the response - persist it, it is shown nowhere else.
// Must be signed by the account's own wallet.
std::pair<nlohmann::json, std::string> approveAgent(
const std::optional<std::string>& name = std::nullopt);
// Later: trade as the agent
auto [result, agent_key] = exchange.approveAgent("mybot");
auto agent_wallet = Wallet::fromPrivateKey(agent_key);
Exchange agent_exchange(agent_wallet, MAINNET_API_URL, nullptr, "",
/*account_address=*/master_address);// Approve a max builder fee rate (percent string) for a builder address.
// Must be signed by the account's own wallet, not an agent wallet.
// Verify with info_.maxBuilderFee(user, builder).
nlohmann::json approveBuilderFee(const std::string& builder,
const std::string& max_fee_rate); // e.g. "0.001%"nlohmann::json updateLeverage(int leverage,
const std::string& coin,
bool is_cross = true);
// Add (positive) or remove (negative) a fixed USDC amount of isolated margin
nlohmann::json updateIsolatedMargin(double amount, const std::string& coin);
// Top up an isolated position so its effective leverage drops to the target.
// The exchange computes the margin to add; this never removes margin.
nlohmann::json topUpIsolatedOnlyMargin(const std::string& coin, double leverage);// Move HYPE from the spot balance into the staking balance. Amounts are wei;
// HYPE has 8 decimals, so 1 HYPE = 100000000 wei.
nlohmann::json cDeposit(uint64_t wei);
// Back to spot. Goes through a 7-day unstaking queue.
nlohmann::json cWithdraw(uint64_t wei);
// Assign staked HYPE to a validator, or take it back. 1-day lockup per
// validator, restarted by each top-up.
nlohmann::json tokenDelegate(const std::string& validator,
uint64_t wei,
bool is_undelegate);Staking is two steps and neither alone earns anything: cDeposit() fills the
staking balance, tokenDelegate() puts it to work. Read the two halves back
with info_.userStakingSummary() (undelegated / delegated) and
info_.userStakingDelegations(). See examples/staking_actions.cpp.
A multi-sig account's actions must all arrive wrapped in multiSig(), carrying
signatures from at least threshold of its authorized users. The wallet that
sends it is the leader: it must itself be an authorized user (or an agent of
one), and its nonce is the one the exchange consumes.
// Convert an account to multi-sig. Signed by the account's own wallet;
// agent wallets are rejected, and every authorized user must already exist
// on Hyperliquid. At most 10 users, threshold in [1, users].
nlohmann::json convertToMultiSigUser(const std::vector<std::string>& authorized_users,
int threshold);
// Send an action on behalf of a multi-sig account.
nlohmann::json multiSig(const std::string& multi_sig_user,
const nlohmann::ordered_json& inner_action,
const std::vector<Signature>& signatures,
int64_t nonce);Every signer signs the same inner action, nonce and leader address. The signing entry point depends on the inner action:
// One nonce for the whole action - everyone signs it, the leader sends it.
int64_t nonce = getTimestampMs();
auto inner = orderWiresToOrderAction({orderRequestToOrderWire(order, asset)},
std::nullopt, "na");
std::vector<Signature> signatures;
for (auto& signer : authorized_wallets) {
// L1 actions (order, cancel, updateLeverage, ...)
signatures.push_back(signMultiSigL1ActionPayload(
*signer, inner, /*vault=*/std::nullopt, nonce, /*expires_after=*/std::nullopt,
multi_sig_user, leader->address(), is_mainnet));
// User-signed actions (sendAsset, convertToMultiSigUser, ...) instead use
// signMultiSigUserSignedActionPayload(). Those inner actions must carry
// their own "signatureChainId" and "hyperliquidChain" - nothing adds them
// on this path.
}
exchange.multiSig(multi_sig_user, inner, signatures, nonce);inner_action is an ordered_json because its key order feeds the signed
msgpack hash. Build it with orderWiresToOrderAction() or in the order the
exchange uses. The nonce, vault and expiresAfter must match across every
signer and the leader.
Inspect an account's signer set with
info_.queryUserToMultiSigSigners(address), which returns null for an
address that is not multi-sig. Changing the signer set, or converting back to
a normal user, also goes through multiSig() — see examples/multi_sig.cpp.
Note: converting to multi-sig does not move the HyperEVM side of the account, which stays controlled by the original key. Multi-sig accounts should not interact with HyperEVM.
// Buy extra request weight instead of being throttled.
// Check current usage with exchange.info_.userRateLimit(address).
nlohmann::json reserveRequestWeight(int64_t weight);// User state (positions, margin)
nlohmann::json userState(const std::string& address, const std::string& dex = "");
// Open orders
nlohmann::json openOrders(const std::string& address, const std::string& dex = "");
// All mid prices
nlohmann::json allMids(const std::string& dex = "");
// User fills
nlohmann::json userFills(const std::string& address);
// Perpetuals metadata
Meta meta(const std::string& dex = "");
// Spot metadata
SpotMeta spotMeta();
// Order book snapshot
nlohmann::json l2Snapshot(const std::string& name);
// Query order
nlohmann::json queryOrderByOid(const std::string& user, int64_t oid);
// Multi-sig signer set, or null if the address is not a multi-sig account
nlohmann::json queryUserToMultiSigSigners(const std::string& multi_sig_user);
// Metadata for every perp dex at once, index-aligned with perpDexs().
// Note: returns bare metas, not the [meta, ctxs] pairs the docs show.
nlohmann::json allPerpMetas();
// Leverage, max trade size and available balance for one user and coin (perps
// only). maxTradeSzs and availableToTrade are [buy, sell] pairs.
nlohmann::json activeAssetData(const std::string& user, const std::string& coin);
// Agent (API) wallets approved for a user. validUntil is null when the agent
// does not expire, despite the docs typing it as an int.
nlohmann::json extraAgents(const std::string& user);Borrow/lend is read-only so far; staking and outcome actions live on
Exchange (see Prediction Markets below). Amounts come back as decimal
strings throughout.
// Staking. The wire types are named for the question rather than the answer,
// so these send "delegatorSummary", "delegations" and "delegatorRewards".
nlohmann::json userStakingSummary(const std::string& user);
nlohmann::json userStakingDelegations(const std::string& user);
nlohmann::json userStakingRewards(const std::string& user);
nlohmann::json delegatorHistory(const std::string& user);
// Borrow/lend. token is a numeric token index (0 is USDC); an ltv of "0.0"
// means the token is not accepted as collateral.
nlohmann::json borrowLendUserState(const std::string& user);
nlohmann::json borrowLendReserveState(int64_t token);
nlohmann::json allBorrowLendReserveStates();
// Prediction markets. settledOutcome returns null while an outcome is still
// open, and also for an index that does not exist.
nlohmann::json outcomeMeta();
nlohmann::json settledOutcome(int64_t outcome);
nlohmann::json outcomeTemplates(); // HIP-4 deploy templates
nlohmann::json outcomeDeployerLimits(const std::string& venue);
// Spot deploy auctions. spotDeployState also reports the deployments the given
// deployer has in flight.
nlohmann::json spotDeployState(const std::string& user);
nlohmann::json spotPairDeployAuctionStatus();Run ./build/examples/staking_lending_and_outcomes [address] for a live tour
of all of these — no private key needed.
Each outcome has two sides, each its own token. Side s of outcome o
(ids from outcomeMeta(); side 0 is the first sideSpecs entry, usually Yes)
trades as the coin #<10*o + s> — asset id 100000000 + 10*o + s — and shows
up in balances as +<10*o + s>. No metadata is needed to resolve these, so the
ordinary order methods take them directly:
#include <hyperliquid/utils/conversions.hpp>
std::string yes = hyperliquid::outcomeCoin(14200, 0); // "#142000"
// Sizes are whole shares; prices sit in (0, 1), are rounded to 5 significant
// figures and at most 5 decimals, and marketOpen() clamps its slippage price
// to [0.00001, 0.99999]. Buying Yes at p is the same order as selling No at 1-p.
exchange.order(yes, true, 100, 0.42, limit_gtc);
exchange.marketOpen(yes, true, 100);
// Convert balances without trading (L1 action "userOutcome").
exchange.splitOutcome(14200, 50); // 50 USDC -> 50 Yes + 50 No
exchange.mergeOutcome(14200); // Yes + No -> USDC; nullopt = max
exchange.mergeQuestion(480); // one Yes of every outcome -> USDC
exchange.negateOutcome(480, 14183, 10); // 10 No -> 10 Yes of each siblingDeployer actions (testnet-only at the time of writing) instantiate templates
from outcomeTemplates(). They are signed without a vault, like the Python
SDK's deploy actions. Keywords are a std::map, so they go out in the sorted
order the exchange requires:
exchange.activateOutcomeDeployer("ab"); // deactivateOutcomeDeployer() is permanent
exchange.registerStandaloneOutcomeFromTemplate(
"ab", {"binaryPrice", {{"perp", "BTC"}, {"threshold", "100000"}, /* ... */}},
/*deployer_fee_scale=*/1);
exchange.registerQuestionFromTemplate("ab", question, 1, {named_a, named_b});
exchange.registerAndAssociateNamedOutcomeFromTemplate("ab", /*question=*/3, named_c);
// name/description and side names must match outcomeMeta() exactly.
exchange.settleOutcome("ab", {7, /*settle_fraction=*/1.0,
{"template:abc", "expiry:...|target:100|underlying:ABC"},
{"template:Over", "template:Under"}});
exchange.settleQuestion("ab", 3, {win, lose}, {"template:abc", "expiry:20260801-1830"});
exchange.setOutcomeSubDeployers("ab", {{"settleOutcome", "0x...", true}});outcomeMetaUpdates streams outcome creations and settlements over the
websocket.
// Create wallet from private key
static std::shared_ptr<Wallet> fromPrivateKey(const std::string& private_key_hex);
// Get Ethereum address
std::string address() const;
// Sign message
Signature signMessage(const std::vector<uint8_t>& message_hash) const;// Good Til Canceled
OrderType gtc;
gtc.limit = LimitOrderType{"Gtc"};
// Immediate or Cancel
OrderType ioc;
ioc.limit = LimitOrderType{"Ioc"};
// Add Liquidity Only (post-only)
OrderType alo;
alo.limit = LimitOrderType{"Alo"};OrderType trigger;
trigger.trigger = TriggerOrderType{
.trigger_px = 2000.0,
.is_market = true,
.tpsl = "tp" // "tp" for take profit, "sl" for stop loss
};TWAP is not an OrderType — it is a separate action with its own method. See
TWAP Operations.
// Buy 0.1 ETH spread over 30 minutes
exchange.twapOrder("ETH", true, 0.1, 30);The SDK automatically handles asset ID mapping:
- Perpetuals: 0-9999 (e.g., BTC = 0, ETH = 1)
- Spot: 10000+ (e.g., PURR/USDC = 10000)
- Builder Perps: 110000+ (e.g., test:ABC = 110000)
try {
auto result = exchange.order(...);
if (result["status"] == "ok") {
// Success
}
} catch (const hyperliquid::ClientError& e) {
// 4xx client error
std::cerr << "Client error: " << e.what() << "\n";
std::cerr << "Code: " << e.errorCode() << "\n";
} catch (const hyperliquid::ServerError& e) {
// 5xx server error
std::cerr << "Server error: " << e.what() << "\n";
} catch (const std::exception& e) {
// Other errors
std::cerr << "Error: " << e.what() << "\n";
}The SDK is organized into several layers:
- Keccak-256: OpenSSL-based hashing
- ECDSA: secp256k1 signing and key management
- EIP-712: Typed data encoding and signing
- Action hash computation (msgpack + nonce + vault + expires)
- L1 action signing (orders, cancels)
- User-signed action signing (transfers)
- API: Base HTTP client using libcurl
- Info: Read-only market data queries
- Exchange: Trading operations
- Float-to-wire conversion (8-decimal precision)
- Hex/bytes conversion
- Timestamp generation
The SDK has been designed to work with both testnet and mainnet:
// Testnet
hyperliquid::Exchange exchange(wallet, hyperliquid::TESTNET_API_URL);
// Mainnet
hyperliquid::Exchange exchange(wallet, hyperliquid::MAINNET_API_URL);
// or simply
hyperliquid::Exchange exchange(wallet); // defaults to mainnetTests mock the HTTP layer, so they need no network and no private key:
cmake -S . -B build -DBUILD_TESTS=ON
cmake --build build
cd build && ctest --output-on-failureTests assert with assert(), and the build defaults to Release (which defines
NDEBUG and would compile every assert away). Test targets therefore force
-UNDEBUG in tests/CMakeLists.txt — keep that on any new test target, or the
test will pass no matter what it claims to check.
OpenSSL not found:
# Ubuntu/Debian
sudo apt-get install libssl-dev
# macOS
brew install openssl@3
export OPENSSL_ROOT_DIR=/usr/local/opt/openssl@3libcurl not found:
# Ubuntu/Debian
sudo apt-get install libcurl4-openssl-dev
# macOS
brew install curl"KECCAK-256 not available":
- Ensure OpenSSL 3.0+ is installed
- Check OpenSSL version:
openssl version
"Invalid private key":
- Ensure private key starts with "0x" or is 64 hex characters
- Private key should be 32 bytes (64 hex chars)
"User or API Wallet does not exist":
- Signature verification failed
- Check that the private key matches the address
- Ensure correct network (mainnet vs testnet)
Any request that can go to /info or /exchange can go over an open websocket
instead. One call switches the transport; every method keeps its signature, its
return shape and its exceptions:
Exchange exchange(wallet, MAINNET_API_URL);
exchange.useWebsocketTransport();
exchange.order("ETH", true, 0.1, 2000.0, order_type); // over the websocket
exchange.info_.l2Snapshot("ETH"); // same socketExchange and the Info it exposes as info_ share one socket, the same way
they already share one HTTP connection. Requests fall back to HTTP while the
socket is down or reconnecting, so a blip costs latency rather than a failed
order. /explorer requests are not supported over the socket by the server and
stay on HTTP.
For requests you do not want to wait on one at a time, the underlying manager takes the same payloads and returns futures:
auto& ws = *info.websocketTransport();
auto book = ws.post("info", json{{"type", "l2Book"}, {"coin", "ETH"}});
auto mids = ws.post("info", json{{"type", "allMids"}});
process(book.get(), mids.get()); // both were in flight at onceWaiting on one of those futures from inside a subscription callback would block the thread that has to deliver the reply, so the blocking path throws rather than hanging if you try.
- Nonce Management: Uses millisecond timestamps for nonces
- Connection Pooling: libcurl reuses connections for efficiency
- Batch Operations: Use bulk methods for multiple orders
- Metadata Caching: Info class caches coin-to-asset mappings
- WebSocket Transport: One socket for streaming and requests, no per-request HTTP headers, and concurrent requests do not queue behind one curl handle
- Hyperlens.io - Hyperliquid data platform with actionable analytics on traders, dexes, builders, and more.
Contributions are welcome! Please ensure:
- Code follows C++17 standards
- Examples compile without warnings
- All tests pass on both testnet and mainnet
- Documentation is updated for new features
For issues and questions:
- Check the official Hyperliquid docs
- Review example code in
examples/directory - Test on testnet before mainnet
Disclaimer: This SDK is for educational and development purposes. Always test thoroughly on testnet before using real funds on mainnet. Trading carries risk.