
How to Find the Wallet Behind a Hyperliquid Order
Turn a Hyperliquid transaction hash or order ID (oid) into the wallet address that placed it. A tested Python script using txDetails, orderStatus, and Dwellir's indexed fills.
A Hyperliquid order ID or transaction hash tells you that something happened, but not who did it. Support teams get an oid from a user, analysts see a large fill on the explorer, and bots log a hash without the address behind it. In this guide we build a Python script that turns either one into the wallet address that placed the order.
There's a trap in this lookup. The public API cannot search by oid, because orderStatus asks for the wallet as an input. A fill's hash can also point to the wrong wallet: both sides of a trade share the taker's hash. The script handles both. It resolves hashes through Hyperliquid's public explorer endpoint, falls back to Dwellir's indexed fills when all you have is an oid, and traces sub-accounts back to the wallet that controls them.
We ran the requests in this guide against Hyperliquid mainnet on 7 October 2026, and the outputs below come from that run.
What you will learn
- Resolve a transaction hash to the signing wallet with
txDetails - Check whether a wallet owns an
oidwithorderStatus - Tell taker fills from maker fills, and spot TWAP fills with a zero hash
- Find the wallet behind a filled
oidwith Dwellir'sallFillsByTime - Trace a sub-account to its master wallet with
userRole - Wrap all of it in one command-line tool
Prerequisites
- Python 3.10 or newer. Run
python3 --versionto check. The script uses only the standard library, so there is nothing to install. - For
oid-only lookups, a Dwellir API key. Create a free account and export the key:
export DWELLIR_API_KEY=your-api-keyThe hash lookups in steps 1 and 2 use Hyperliquid's public endpoints and need no key.
Which lookup do you need?
| You have | Lookup | Returns |
|---|---|---|
| Transaction hash | Public txDetails on rpc.hyperliquid.xyz/explorer | The wallet that signed the transaction, plus its action |
oid and a candidate wallet | Public orderStatus | The order if the wallet owns it and the order is recent, unknownOid if not |
oid, market, and rough time of a filled order | Dwellir allFillsByTime | The user on the matching fill |
| A trade from the last few seconds | Public recentTrades | Buyer and seller in users |
| An order that never filled | Dwellir gRPC order statuses or the archive | The user on each status event |
Step 1: Resolve a transaction hash
We'll start with the case that needs one request. Hyperliquid's explorer endpoint answers txDetails with the transaction, including the user that signed it. Note the host: txDetails lives on rpc.hyperliquid.xyz, and the same path on api.hyperliquid.xyz returns 404.
Create find_wallet.py:
import json
import urllib.request
INFO_URL = "https://api.hyperliquid.xyz/info"
EXPLORER_URL = "https://rpc.hyperliquid.xyz/explorer"
ZERO_HASH = "0x" + "0" * 64
def post(url, body):
req = urllib.request.Request(
url, data=json.dumps(body).encode(), headers={"Content-Type": "application/json"}
)
with urllib.request.urlopen(req, timeout=30) as resp:
return json.load(resp)
def wallet_from_hash(tx_hash):
"""Return the account that signed a HyperCore transaction, with its action and time."""
if tx_hash == ZERO_HASH:
raise ValueError("zero hash: this fill came from a TWAP slice, not a user transaction")
tx = post(EXPLORER_URL, {"type": "txDetails", "hash": tx_hash})["tx"]
return tx["user"], tx["action"], tx["time"]post is the only network helper we need. Every Hyperliquid read in this guide is a JSON POST with a type field. The zero-hash check comes from what we saw on mainnet: TWAP slice fills carry a hash of all zeros, and txDetails rejects it with invalid block height: 0.
Try it from a Python shell in the same folder:
>>> from find_wallet import wallet_from_hash
>>> wallet_from_hash("0xd6587247f0344af0d7d204460a40ef0201e8002d8b3769c27a211d9aaf3824db")You should see the signer, the action, and the time:
('0x0e08e4e25416af46e5042af3a01e92777b897379', {'type': 'order', 'orders': [{'a': 0, 'b': True, 'p': '87676', 's': '0.00189', 'r': False, 't': {'limit': {'tif': 'Ioc'}}, 'c': '0x0200001401000002f2c77fccd0f7059a'}], 'grouping': 'na', 'builder': {'b': '0x557edb253b1d7ed5f15b248a5a3fd919fa5d3c81', 'f': 35}}, 1791378463783)The action is an IOC buy on asset 0 (BTC), with client order ID c, routed through a builder. You can also paste a hash into the search box on the Hyperliquid explorer to see the same wallet.
Step 2: Check who owns an order
A hash tells us who signed a transaction, but a fill's hash isn't always the fill owner's own transaction. When a trade matches, both the taker's fill and the maker's fill carry the hash of the taker's order. Here's the trade behind our example hash, tid 128437475437687:
| Side | Wallet | oid | crossed |
|---|---|---|---|
| Taker (buy) | 0x0e08...7379 | 567668916418 | true |
| Maker (sell) | 0x6540...4268 | 567668910767 | false |
Run txDetails on the maker's fill and you get the taker's wallet. To catch that, we check ownership with orderStatus. It takes user and oid and returns unknownOid for any wallet that doesn't own the order. Add this function to find_wallet.py, below wallet_from_hash:
def owns_order(user, oid):
"""True if `oid` belongs to `user`. orderStatus returns unknownOid for any other wallet."""
status = post(INFO_URL, {"type": "orderStatus", "user": user, "oid": oid})
return status["status"] == "order"Check both sides of the trade against the signer:
>>> from find_wallet import owns_order
>>> signer = "0x0e08e4e25416af46e5042af3a01e92777b897379"
>>> owns_order(signer, 567668916418), owns_order(signer, 567668910767)
(True, False)The taker's oid belongs to the signer and the maker's doesn't. orderStatus also accepts a client order ID as a hex string in the oid field, which helps when your logs store cloid instead.
Important: orderStatus only remembers a wallet's recent orders. The maker in this trade is a busy market-making sub-account whose historicalOrders covered about the last 9 minutes. An hour and a half after the trade, orderStatus returned unknownOid for the maker's order even with the maker's own wallet, while the quieter taker's order still resolved. Treat unknownOid as "not found", not as proof that the wallet is wrong.
Tip: if you have the fill itself, read crossed first. true means the fill is the taker side and the hash is that wallet's own transaction.
Step 3: Find the wallet from an oid alone
orderStatus can't help when all we have is an oid: it needs the wallet as input, and a request without user fails with Failed to deserialize the JSON body into the target type. We need data that carries both oid and user. Dwellir's historical fills do: each row from allFillsByTime includes user, oid, hash, and cloid.
allFillsByTime caps each window at one hour, so we search 30 minutes either side of the time we have and filter to one market with coin. Full pages hold 2,000 rows. While a page comes back full, we build the next cursor from the last row's time and txIndex, keeping endTime fixed.
Add import os to the top of the file, then add these below owns_order:
MAX_WINDOW_MS = 30 * 60 * 1000
def index_fills(coin, around_ms, api_key, window_ms=MAX_WINDOW_MS):
"""Yield every indexed fill for `coin` within `window_ms` either side of `around_ms`."""
url = f"https://api-hyperliquid-index.n.dwellir.com/{api_key}/info"
end = around_ms + window_ms
cursor = None
while True:
body = {"type": "allFillsByTime", "coin": coin, "endTime": end, "limit": 2000}
body.update({"cursor": cursor} if cursor else {"startTime": around_ms - window_ms})
fills = post(url, body)
yield from fills
if len(fills) < 2000:
return
cursor = f"{fills[-1]['time']}_{fills[-1]['txIndex']}"
def wallet_from_oid(oid, coin, around_ms, api_key, window_ms=MAX_WINDOW_MS):
"""Return the wallet that owns a filled order, or None if no fill in the window matches."""
for fill in index_fills(coin, around_ms, api_key, window_ms):
if fill["oid"] == oid:
return fill["user"]
return Noneindex_fills is a generator, so the search stops at the first page that contains a match. The window decides the cost, because the Index bills one API credit per returned fill. Here's what three windows around our example BTC trade returned:
| Window around the trade | Fills returned | Time |
|---|---|---|
| 1 second either side | 6 | about 1 s |
| 60 seconds either side | 566 | about 2 s |
| 30 minutes either side (the maximum) | about 14,000, over 7 pages | 11 to 23 s |
Start with the narrowest window you can justify and widen it only if the search comes back empty. When you got here from a transaction hash, you already know the exact time: the fill shares the transaction's timestamp.
Important: match on coin and oid together, not oid alone. Dwellir's order-status feeds have seen HIP-4 outcome markets reuse an oid across the two sides of one market. The coin filter in the request covers that here.
Step 4: Trace a sub-account to its master wallet
The wallet on a fill can be a sub-account. Its trades are controlled by a master wallet, which is usually the address you're really after. userRole tells us which kind of account we have. Add this below owns_order:
def master_of(user):
"""Return the master wallet if `user` is a sub-account, else None."""
role = post(INFO_URL, {"type": "userRole", "user": user})
return role["data"]["master"] if role["role"] == "subAccount" else NoneCheck the maker wallet from our example trade:
>>> from find_wallet import master_of
>>> master_of("0x654086857e1fad6dcf05cf6695cce51ea3984268")
'0xe4c6ae25959d7fc66cf2dd5965fb78c5e09c4048'For a regular wallet, userRole returns {"role":"user"} and the function returns None.
Putting it all together
Now we give the script a command line that picks the right path. With --hash, it resolves the signer. With --hash and --oid, it checks whether the oid is the signer's order. If it isn't, the order is usually the maker side of the trade, so the script searches the Index around the transaction time. With --oid, --coin, and --time, it goes straight to the Index, searching --window seconds either side (30 minutes by default). Whenever the Index finds the owner, the script also checks whether it is a sub-account and prints the master wallet.
Add import argparse to the top of the file, then add this at the bottom:
def main():
parser = argparse.ArgumentParser(description="Find the wallet behind a Hyperliquid order")
parser.add_argument("--hash", help="transaction hash from a fill or the explorer")
parser.add_argument("--oid", type=int, help="order ID")
parser.add_argument("--coin", help="market symbol, for example BTC (needed with --oid alone)")
parser.add_argument("--time", type=int, help="approximate order time in Unix ms (needed with --oid alone)")
parser.add_argument("--window", type=int, default=1800, help="seconds to search either side of --time, max 1800")
args = parser.parse_args()
if args.hash:
user, action, tx_time = wallet_from_hash(args.hash)
print(f"signer: {user} ({action['type']} at {tx_time})")
if args.oid is None:
return
if owns_order(user, args.oid):
print(f"oid {args.oid} belongs to {user}")
return
print(f"oid {args.oid} is not the signer's order, searching fills for its owner")
args.time = args.time or tx_time
args.window = 1 # the fill shares the transaction's timestamp
if args.oid is not None:
api_key = os.environ.get("DWELLIR_API_KEY")
if not (api_key and args.coin and args.time):
parser.error("--oid lookups need --coin, --time and DWELLIR_API_KEY")
window_ms = min(args.window, 1800) * 1000
user = wallet_from_oid(args.oid, args.coin, args.time, api_key, window_ms)
if user is None:
print(f"no fill for oid {args.oid} on {args.coin} within {window_ms // 1000}s of {args.time}")
return
print(f"oid {args.oid} belongs to {user}")
master = master_of(user)
if master:
print(f"{user} is a sub-account of {master}")
if __name__ == "__main__":
main()Run it with a hash:
python3 find_wallet.py --hash 0xd6587247f0344af0d7d204460a40ef0201e8002d8b3769c27a211d9aaf3824dbsigner: 0x0e08e4e25416af46e5042af3a01e92777b897379 (order at 1791378463783)Add the taker's oid to confirm ownership:
python3 find_wallet.py --hash 0xd6587247f0344af0d7d204460a40ef0201e8002d8b3769c27a211d9aaf3824db --oid 567668916418signer: 0x0e08e4e25416af46e5042af3a01e92777b897379 (order at 1791378463783)
oid 567668916418 belongs to 0x0e08e4e25416af46e5042af3a01e92777b897379Now the maker's oid from the same trade. The script notices the signer doesn't own it and searches the Index around the transaction time:
python3 find_wallet.py --hash 0xd6587247f0344af0d7d204460a40ef0201e8002d8b3769c27a211d9aaf3824db --oid 567668910767 --coin BTCsigner: 0x0e08e4e25416af46e5042af3a01e92777b897379 (order at 1791378463783)
oid 567668910767 is not the signer's order, searching fills for its owner
oid 567668910767 belongs to 0x654086857e1fad6dcf05cf6695cce51ea3984268
0x654086857e1fad6dcf05cf6695cce51ea3984268 is a sub-account of 0xe4c6ae25959d7fc66cf2dd5965fb78c5e09c4048That run took about 1 second: the script searched one second either side of the transaction time and read 6 fills.
With only an oid, pass the market, an approximate time, and how many seconds to search either side:
python3 find_wallet.py --oid 567668910767 --coin BTC --time 1791378463783 --window 60oid 567668910767 belongs to 0x654086857e1fad6dcf05cf6695cce51ea3984268
0x654086857e1fad6dcf05cf6695cce51ea3984268 is a sub-account of 0xe4c6ae25959d7fc66cf2dd5965fb78c5e09c4048If nothing matches, the script says so instead of guessing:
no fill for oid 567668910767 on ETH within 5s of 1791378463783What if the order never filled?
Fill searches can't find orders that never traded. Open, canceled, and rejected orders show up only as order status events, and each event carries user, oid, and coin:
- For recent orders, replay Dwellir's gRPC
StreamOrderStatusesor callGetOrderStatusesfrom a block number near the order time, with acoinsfilter. - For older orders, the
node_order_statuses_by_blockdataset in the Hyperliquid historical archive holds every status event since 22 January 2026.
For trades from the last few seconds, the public recentTrades request returns the buyer and seller in users, with the trade's hash and tid. It only returns the last 10 trades per market, so it suits live monitoring, not lookups after the fact.
Going to production
- Retries: the script raises on any HTTP error. Wrap
postwith a retry and backoff for429and5xxresponses before you run it unattended, and let403(a bad API key) fail fast. - Rate limits:
orderStatusandtxDetailsgo to Hyperliquid's public endpoints, which limit requests per IP. A support tool that checks one order at a time is fine. For bulk attribution, pull fills from the Index and skip per-orderorderStatuscalls. - Cost: the Index bills one API credit per returned fill, and a full hour of BTC returned about 14,000. Start narrow, widen only on a miss, and cache answers by
(coin, oid). - Keys: read
DWELLIR_API_KEYfrom the environment or a secret store. It is part of the request URL, so keep it out of logs. - Confirmation: use
orderStatusfor fresh orders only. For anything older than a few minutes on a busy wallet, the Index fill is the record to trust.
Next steps
- Pull the wallet's full trade history with
userFillsByTime, or its open positions withclearinghouseState. - Watch a set of wallets in real time, as in How to track Hyperliquid whales.
- Read How Hyperliquid's infrastructure works to see where orders, fills, and API servers sit.
This guide used Hyperliquid's public explorer and Info endpoints and Dwellir's Hyperliquid Index. To run the oid lookups yourself, create a free Dwellir account.
Frequently Asked Questions
How do I find the wallet that placed a specific order on Hyperliquid?
If you have the transaction hash, send a txDetails request to https://rpc.hyperliquid.xyz/explorer and read tx.user. If you only have the order ID, search indexed fills for that oid with Dwellir's allFillsByTime and read the user field. If that wallet is a sub-account, userRole returns its master wallet.
Can I look up a Hyperliquid order by oid alone?
Not on the public API. orderStatus requires the wallet address as an input and returns unknownOid for any wallet that does not own the order. You need a fill or order-status dataset that carries a user field, such as Dwellir's Hyperliquid Index or gRPC order statuses.
Why does txDetails return the wrong wallet for my fill?
Both sides of a Hyperliquid trade carry the taker's transaction hash. If your fill has crossed set to false, you are the maker, and txDetails on that hash returns the taker's wallet.
Why does orderStatus return unknownOid for an order I know exists?
orderStatus only remembers a wallet's recent orders. For a busy market-making sub-account in our test, that was about the last 9 minutes, so a 90-minute-old filled order returned unknownOid even with the right wallet. Use indexed fills for older orders.
What does a fill with a zero transaction hash mean?
A hash of all zeros means no user transaction caused the fill. In our tests these were TWAP slice fills. txDetails rejects the zero hash, so use the fill's user field or the trade's users pair instead.
How do I find the wallet for a Hyperliquid order that never filled?
Open, canceled, and rejected orders produce no fill. Each order status event carries user, oid, and coin, so use Dwellir's gRPC order-status feeds for recent blocks or the node_order_statuses_by_block archive dataset for older history.
How to Track Hyperliquid Whales in Real Time
Build a Hyperliquid whale tracker in Python. Stream large fills over gRPC, surface big resting orders, and read whale positions through Dwellir's read infra.
Polymarket API: The Complete Developer Guide
A developer's guide to the Polymarket API: discover markets with Gamma, read prices and place orders on the CLOB, stream fills, and read positions on Polygon.