skip to content
TTL ZERO
Table of Contents

Subject: the Hyperliquid web frontend (a saved copy of the full page set) Revision: 2026-08-01-40f25a22d (from REACT_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 judged

The 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 well

So 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 he is true, 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 history
if (n.isCctp && j.has(t.toLowerCase())) return; // skip blocked addresses

The 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

  1. Sanctioned walletuserIpBlocked
  2. Trading blocked by IP (ipAllowed = false) → ipBlocked
  3. IP check still in flight → ipLoading
  4. Not connected → disconnected
  5. Terms not accepted → needToAcceptTerms
  6. Individually disallowed (userAllowed = false) → userBlocked
  7. 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 screen
Hn(e): ipLoading / ipBlocked / userIpBlocked → true
// (b) the message shown to the user
Vn(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 / selfie
  • sanctions_check
  • pep_check (Politically Exposed Person)
  • negative_news_check
  • tax_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 from legalCheck, and expands into the three flags ipAllowed, outcomeMarketsAllowed and referralsAllowed. It is live only on Mainnet.
  • Wallet restriction is an exact-match block against the 24-address Set (variable j) embedded in the client.
  • The two are merged into state machine D and 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_check and 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 legalCheck and 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.