Inside Hyperliquid's frontend regulation logic
/ 6 min read
Read this post in 日本語Table of Contents
Subject: the Hyperliquid web frontend (a saved copy of the full page set) Revision:
2026-08-01-40f25a22d(fromREACT_APP_VERSION) Main bundle:main.8c49febc.js(about 4.9 MB, minified) Method: static analysis (token extraction and context reconstruction from the minified JavaScript) Caveat: this report contains inferences drawn from static analysis of client-side code. The real enforcement happens server-side.
Overview (TL;DR)
The Hyperliquid frontend concentrates its regulatory decisions in the TraderState provider of the main app (function L inside main.8c49febc.js), and builds them out of three layers.
| Layer | Decision axis | Data source |
|---|---|---|
| 1. IP-based geo restriction | where the requesting IP is located | server API legalCheck |
| 2. Wallet-address restriction | whether the connected wallet matches a sanctions list | a fixed Set embedded in the client |
| 3. Per-user allowance / terms acceptance | userAllowed / acceptedTerms |
the same legalCheck response |
The three layers are merged into state machine D, evaluated in priority order, and reduced to a single activeState string. Whenever the outcome is a block, the UI wording converges on “Not available in your jurisdiction”.
Layer 1: IP-based geo restriction (server-side legalCheck)
The API call
When the provider mounts, it queries the /info endpoint.
await c0t({ endpoint: "info", request: { type: "legalCheck", user: l ?? i.I4A }});// l = the connected wallet address. When disconnected, i.I4A (a dummy address) is sent, so only the IP is judgedThe response is validated against schema T, whose shape is as follows.
T = { acceptedTerms: bool, userAllowed: bool, restrictions: enum }The actual geolocation of the IP happens on the server; the frontend receives only an enum value that says how to restrict. The IP address itself is never handled on the JavaScript side.
The restrictions enum (four values)
A = { NoRestrictions: "n", BlockActions: "a", HideOutcomes: "o", Uk: "u"};When IP restrictions apply
J = ("Mainnet" === chain) || i.PA7; // performIpRestrictions// env: REACT_APP_FORCE_MAINNET_IP_RESTRICTIONS can force it on as wellSo IP restrictions are live only on Mainnet. On Testnet and Local, performIpRestrictions = false, and every check downstream short-circuits to “allowed” (if (!n) return true;).
Deriving three flags from restrictions
The provider’s return value (useMemo) converts restrictions into three independent permission flags.
return { // ... ipAllowed: X, // whether trading and actions themselves are permitted outcomeMarketsAllowed: ee, // whether outcome (prediction) markets may be displayed referralsAllowed: ne, // whether referrals are permitted userAllowed: se};The boolean value of each flag (with performIpRestrictions = true) is decided by a switch, and comes out as follows.
restrictions |
ipAllowed |
outcomeMarketsAllowed |
referralsAllowed |
Meaning |
|---|---|---|---|---|
NoRestrictions (n) |
✅ | ✅ | ✅ | no restriction |
BlockActions (a) |
❌ | ✅ | ✅ | all trading blocked (assumed to target sanctioned countries, the US and the like) |
HideOutcomes (o) |
✅ | ❌ | ✅ | only the outcome (prediction) markets are hidden |
Uk (u) |
✅ | ✅ | ❌ | UK: trading allowed but referrals prohibited (presumed to address the FCA’s financial promotion rules) |
The design therefore steps through the levels region by region: a blanket ban, hiding one product only, or switching referrals off for the UK.
Layer 2: wallet-address restriction (a blocklist embedded in the client)
main.8c49febc.js hardcodes a Set of 24 lowercase addresses (variable j), assumed to be sanctioned or frozen addresses.
const j = new Set([ "0x6a01af210dce01f44b5067fc688641384a8bce5b", "0xf587b821d609605b1b124d8e248088e058266784", "0x00071360d75385c4c25dd8f217465693ffe91a69", "0x333983eb213d132cf4f71751dd38802d0362e3fa", "0x7dd0ddff0d845660a1244dcf120c7bb586785835", "0x9bd3e7e58e9d660716a1116a8aeac77a454baad8", "0x00c07d5370605db4d449f389d1aaad3b2ee78e15", "0x4f0f2f8a3f359a704998074832ffff98ca48c28f", "0x8bc964e36e3fcf9068d98e55a64a99d22c898afa", "0x4f91e6b80cb21d8e6ddf21936806d4c03ab12c42", "0x594cccf29daccdcdf36870fd02e50733ace09e49", "0xcefc6c5f2193821db8a77b4a4e388e4e322da141", "0x7899f00dd07a577d11dc9f4a845b46b71d5298a1", "0x6d569cb3481412b57b824e01bc8583b6733e31ef", "0xbb976cda4cdc8141c2fa36129545645bb97b0cc2", "0x62ada4c40cfb35162c81b06f85997dbb48adeb37", "0xa2c5a179777e5caceb6bb9d0cebe7e2a1826ad45", "0x3711f5ce6e55692f1767a1152170efec59756909", "0x49da31ffd54067ca069b7c02f145b3f2185a97e3", "0x68da925495adb0ce3a3477213ac0390b80fb958a", "0xc49acbbf8b98d1ec2296d90afaea14802e7ce4f3", "0x4a8bf1b7bf0ceaa309b49359a111ab31381e8ede", "0xa6fb971f3b7a9b9f76eda76bc89268fe26560189", "0xa0c0e9f307b5a26ca3fb5891c19154fc7a02bef7"]);
const he = j.has((l ?? "").toLowerCase()); // does the connected wallet match?- Once
heistrue, the state machine described below produces the highest-priority block,userIpBlocked. - Separately, the deposit and withdrawal history (CCTP transfers) also uses
j.has(...)while rendering, to drop the entries of matching addresses.
// while rendering the deposit and withdrawal historyif (n.isCctp && j.has(t.toLowerCase())) return; // skip blocked addressesThe final verdict: state machine D
Function D collapses layers 1 to 3 into a single state string. It is called as D(u, he, X, ie, se, ue, de, Z, pe). The priority order, evaluated from the top down, is the heart of the whole scheme.
function D(e, t, n, r, i, o, s, a, c) { return t // t = he : the wallet matches the sanctions list ? "userIpBlocked" : (false === n) // n = X = ipAllowed is false (BlockActions) ? "ipBlocked" : ("unknown" === n) ? "ipLoading" : e // e = u : whether a wallet is connected ? ("unknown" === r) ? "acceptTermsLoading" : (false === r) ? "needToAcceptTerms" // r = acceptedTerms : (s || N(a, c)) ? (false === i) ? "userBlocked" // i = userAllowed is false : o ? "needToDeposit" : N(a, c) ? "readyToTrade" : "noRegisteredAgent" : "noPendingAgent" : "disconnected";}Evaluation priority
- Sanctioned wallet →
userIpBlocked - Trading blocked by IP (
ipAllowed = false) →ipBlocked - IP check still in flight →
ipLoading - Not connected →
disconnected - Terms not accepted →
needToAcceptTerms - Individually disallowed (
userAllowed = false) →userBlocked - From there on: deposit, agent registration,
readyToTrade(the normal path)
How the state reaches the UI (three helpers)
// (a) whether to show the IP block screenHn(e): ipLoading / ipBlocked / userIpBlocked → true
// (b) the message shown to the userVn(e): ipBlocked | userIpBlocked → "Not available in your jurisdiction" needToAcceptTerms → "Need to accept terms" ipLoading | acceptTermsLoading → "Loading..."
// (c) whether to disable the trade buttons and so on (assumes performIpRestrictions)Wn(e): ipBlocked / userIpBlocked / needToAcceptTerms / userBlocked / ... → true (= actions blocked)So a sanctioned address and an IP block are both surfaced to the user through the same text, “Not available in your jurisdiction”. That the UI draws no distinction between the two is a notable trait.
The decision flow
legalCheck (server) ──► restrictions {n,a,o,u} │ ┌─────────────────────┼─────────────────────┐ ▼ ▼ ▼ ipAllowed(X) outcomeMarketsAllowed(ee) referralsAllowed(ne) │ │ he = j.has(wallet) ← sanctioned-address Set embedded in the client │ │ ▼ ▼ ┌───────────────── state machine D ─────────────────┐ │ he → userIpBlocked (highest priority) │ │ !ipAllowed → ipBlocked │ │ !connected → disconnected │ │ !acceptedTerms→ needToAcceptTerms │ │ !userAllowed → userBlocked │ │ ... → needToDeposit / readyToTrade │ └───────────────────────────────────────────────────┘ │ ▼ Hn / Vn / Wn → UI (block screen, wording, action suppression)Aside: a separate KYC / sanctions-check layer
A different bundle, 1123-3d5494c697688080.js, defines the following validation schemas (Zod-style).
identity/residence/selfiesanctions_checkpep_check(Politically Exposed Person)negative_news_checktax_id
This is the KYC / AML layer serving the embedded wallet (Privy) and the fiat path, and it is a separate system from the trading-screen gate of layers 1 to 3 above.
Summary
- IP restriction is driven by the four-valued
restrictions(n/a/o/u) that the server returns fromlegalCheck, and expands into the three flagsipAllowed,outcomeMarketsAllowedandreferralsAllowed. It is live only on Mainnet. - Wallet restriction is an exact-match block against the 24-address
Set(variablej) embedded in the client. - The two are merged into state machine
Dand evaluated in the order sanctioned wallet > IP block > terms acceptance > individual disallow. The UI wording on a block converges on “Not available in your jurisdiction”. - The KYC and sanctions checks (
sanctions_check,pep_checkand so on) live in a separate bundle, as an independent layer for the embedded wallet.
⚠️ Important: every one of these is client-side display and interaction suppression; the real enforcement happens on the server, in
legalCheckand the order-acceptance API. The design appears to be one in which tampering with the client code alone cannot get around the actual trading restrictions.