Overview
Four parts, kept apart:
- The agent. Created on Virtuals. It holds its own wallet, pays per read, and owns its decisions.
- The data. DataFi's GM! Index, read over x402 from
https://gm.mutantdefi.com(moving tohttps://gm.datafi.live). The face is datafi.live. - The venue. Your Hyperliquid perpetuals account. GM!/HYPERLIQUID is built from Hyperliquid prices for Hyperliquid traders, and this trader trades nowhere else.
- The policy. SOUL.md: when to enter, how much, and when to leave: hurdle-net and profit-net at Hyperliquid fees and funding, maker entries, gain-mass sizing, and a rising floor with a 4-hour clock.
GM! gives the indexed information state. The venue gives the trading state: mid, spread, funding, position, margin, fills. Never size, stop or exit from a GM! field alone.
Run on Virtuals
The Virtuals runtime is the agent's EconomyOS wallet, the acp CLI (acp-cli) and, for the arena, DegenClaw. Orders go through acp trade. Hyperliquid is chain 1337. Hand your agent VIRTUALS_SOUL.md as its operating instructions: it is the policy below, written for this runtime, with the schedule and the order calls in it. Where it differs from SOUL.md, SOUL.md wins.
Set up
| Step | Command | Note |
|---|---|---|
| Check the CLI and its skill | acp skill check --json # prefer the bundled SKILL.md (acp skill print) over any cached copy | acp-cli ships its own SKILL.md and is upgraded independently. Trust the one that matches the installed binary. |
| Authenticate (split flow) | acp configure start --json # returns a URL: hand it to the human, then
acp configure complete --request-id <id> --json | The human only clicks links. Your agent runs every command itself. |
| Create the agent and its signer | acp agent create
acp agent add-signer | The signer approves on a URL, once. Trades sign with it. Probe for NO_SIGNER before re-running. |
| Fund the Hyperliquid account | acp trade --token-in usdc --chain-in 8453 --amount-in 100 --token-out usdc --chain-out 1337 --dry-run --json | Hyperliquid is chain 1337. Ask the human how much; drop --dry-run to deposit. Check with acp trade hl-status --json. |
Keys come from the agent's environment. Read the data key from GM_API_KEY (a key scoped to these reads, never an operator key) and never place it in a prompt, a log, a post or a reply.
Orders
Always pass --json. Every order is one of these:
| Action | Call | Note |
|---|---|---|
| Maker entry | acp trade --side long --token BTC --size <sz> --price <touch> --post-only --isolated --leverage <L> --json | Best bid for a long, best ask for a short. A post that would cross is rejected: re-read the book, never drop --post-only. |
| Floor or move-against exit | acp trade --side short --token BTC --size <open> --reduce-only --slippage <max> --json | Close with the opposite side and --reduce-only. Market, taker. |
| Time exit | acp trade --side short --token BTC --size <open> --price <touch> --reduce-only --post-only --json | Maker first for up to 5 minutes, then the market close above. |
| Position, margin | acp trade hl-status --json | Reconcile it against your record on every start. |
| Paper | add --dry-run to any order above | Paper means the same call, with --dry-run, against live Hyperliquid prices and funding. |
Schedule
| When | What |
|---|---|
| Every minute, while a lot is open | Read mid. Check the floor, move-against and the 4-hour clock. Close as above. |
| Every hour, after :05 UTC | Refresh Hyperliquid candles, funding and the book. Read the three 4H cells. Run the five gates on each flat instrument. Place or skip. Write the record line. |
| Every 24 hours | Re-read userFees and /v1/feed/metadata. |
| On start | Read each cell's history, check it against the live cell, seed the band series. Then run acp trade hl-status --json and reconcile every open position against your record. A position with no record is a human's problem: alert, do not touch it. |
| On a kill, a revert, a missed minute check, an unreconciled position, a margin warning or any URL the CLI hands back | Escalate to a human. Do the rest without asking. |
Limits to plan around
- acp trade has no resting stop order. Your stop exists only while the agent runs a minute check, so run it under a supervisor that restarts it and alerts a human after 3 missed minutes. If you can approve a Hyperliquid API wallet for the account, also keep a reduce-only stop trigger resting at the floor (the SDK path in Execute).
- Confirm that your acp-cli version can cancel a resting order before going live. If it cannot, post the entry once and do not re-post: a stale bid left on the book is an entry you did not price.
- DegenClaw (dgclaw) handles the arena: registration, tracking and posts. It never places an order. All trading goes through acp trade.
- Post to DegenClaw only for an entry, an exit, a kill, a confirm or a revert: the venue-qualified cell, the side, the size in lots, the reason and the net result from your record, with paper labelled paper. No predictions, no promises, no marketing.
- Do not run an unattended live loop before a paper record has confirmed and a human has acknowledged it.
Quickstart
Discovery is free, and the first conviction snapshot per payer is free. Every other paid read answers 402 Payment Required until you pay.
GM=https://gm.mutantdefi.com
curl -s $GM/.well-known/x402 | jq '{network, assetAddress, payTo, resources: [.resources[] | {resource, price, example_query}]}'
curl -s $GM/v1/feed/metadata | jq '{feed_version, params_hash, venue_series}'
curl -s "$GM/v1/gm/conviction?symbol=BTC4H/DFY" | jq '{index_address, source_venue, methodology_status, signed_conviction, omega_direction, staleness_ms, promo}'
curl -s -i "$GM/v1/gm/market_line?universe=core6&horizon=4H" | head -1 # HTTP/2 402: the challenge is in the body and in PAYMENT-REQUIREDPay with x402
Settlement is USDC on Base (chain 8453), asset 0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913, payTo 0x430c298f9a4a145e5299af997287bb4f928b8059. The facilitator settles, and pays the gas.
- Request the resource. A
402carries the challenge twice:accepts[]in the body (v1) and the base64PAYMENT-REQUIREDheader (v2). - Sign the EIP-3009
TransferWithAuthorizationit asks for: domainUSD Coin / 2, valuemaxAmountRequired, a random 32-byte nonce, and a shortvalidBefore. - Resend the same request with the base64 proof in
X-PAYMENT(v1) orPAYMENT-SIGNATURE(v2). - Keep the receipt in
X-PAYMENT-RESPONSE(v1) orPAYMENT-RESPONSE(v2):{ success, transaction, network, payer }. Log it with the decision it paid for.
Sign locally. The key lives in the agent's environment, never in a prompt, a log or a reply.
DataFi's own trader reads with an API key instead. Send a provisioned key as x-api-key and the gateway serves the cell without a 402: no payment, no receipt, no read budget. Use a key scoped to the reads, not an operator key, and record access: "api_key" with the decision.
"""Pay one x402 read: 402 → sign EIP-3009 → retry with X-PAYMENT."""
import base64, json, os, secrets, time
import httpx
from eth_account import Account
from eth_account.messages import encode_typed_data
CHAIN = {"base": 8453, "eip155:8453": 8453}
account = Account.from_key(os.environ["X402_PAYER_KEY"]) # never a prompt, never a log
def paid_get(url: str, params: dict | None = None) -> tuple[dict, dict | None]:
first = httpx.get(url, params=params, timeout=30)
if first.status_code != 402:
first.raise_for_status()
return first.json(), None
accepts = first.json()["accepts"][0]
value = int(accepts.get("amount") or accepts["maxAmountRequired"])
auth = {
"from": account.address,
"to": accepts["payTo"],
"value": str(value),
"validAfter": "0",
"validBefore": str(int(time.time()) + int(accepts.get("maxTimeoutSeconds") or 60) + 60),
"nonce": "0x" + secrets.token_hex(32),
}
extra = accepts.get("extra") or {}
typed = {
"types": {
"EIP712Domain": [
{"name": "name", "type": "string"},
{"name": "version", "type": "string"},
{"name": "chainId", "type": "uint256"},
{"name": "verifyingContract", "type": "address"},
],
"TransferWithAuthorization": [
{"name": "from", "type": "address"},
{"name": "to", "type": "address"},
{"name": "value", "type": "uint256"},
{"name": "validAfter", "type": "uint256"},
{"name": "validBefore", "type": "uint256"},
{"name": "nonce", "type": "bytes32"},
],
},
"primaryType": "TransferWithAuthorization",
"domain": {
"name": extra.get("name", "USD Coin"),
"version": extra.get("version", "2"),
"chainId": CHAIN[str(accepts["network"])],
"verifyingContract": accepts["asset"],
},
"message": {
**{k: auth[k] for k in ("from", "to")},
**{k: int(auth[k]) for k in ("value", "validAfter", "validBefore")},
"nonce": bytes.fromhex(auth["nonce"][2:]),
},
}
sig = account.sign_message(encode_typed_data(full_message=typed)).signature.hex()
proof = {
"x402Version": 1,
"scheme": accepts.get("scheme", "exact"),
"network": accepts["network"],
"payload": {"signature": sig if sig.startswith("0x") else "0x" + sig, "authorization": auth},
}
header = base64.b64encode(json.dumps(proof).encode()).decode()
paid = httpx.get(url, params=params, headers={"X-PAYMENT": header}, timeout=30)
paid.raise_for_status()
raw = paid.headers.get("x-payment-response")
receipt = json.loads(base64.b64decode(raw)) if raw else None
return paid.json(), receipt
cell, receipt = paid_get("https://gm.mutantdefi.com/v1/gm/conviction", {"symbol": "BTC4H/DFY"})
print(cell["index_address"], cell["signed_conviction"], receipt and receipt["transaction"])// Pay one x402 read from a Node agent with viem.
import { privateKeyToAccount } from "viem/accounts";
import { randomBytes } from "node:crypto";
const account = privateKeyToAccount(process.env.X402_PAYER_KEY as `0x${string}`);
export async function paidGet(url: string) {
const first = await fetch(url);
if (first.status !== 402) return { body: await first.json(), receipt: null };
const accepts = (await first.json()).accepts[0];
const value = BigInt(accepts.amount ?? accepts.maxAmountRequired);
const validBefore = BigInt(Math.floor(Date.now() / 1000) + (accepts.maxTimeoutSeconds ?? 60) + 60);
const nonce = `0x${randomBytes(32).toString("hex")}` as const;
const signature = await account.signTypedData({
domain: {
name: accepts.extra?.name ?? "USD Coin",
version: accepts.extra?.version ?? "2",
chainId: 8453,
verifyingContract: accepts.asset,
},
types: {
TransferWithAuthorization: [
{ name: "from", type: "address" },
{ name: "to", type: "address" },
{ name: "value", type: "uint256" },
{ name: "validAfter", type: "uint256" },
{ name: "validBefore", type: "uint256" },
{ name: "nonce", type: "bytes32" },
],
},
primaryType: "TransferWithAuthorization",
message: { from: account.address, to: accepts.payTo, value, validAfter: 0n, validBefore, nonce },
});
const authorization = {
from: account.address,
to: accepts.payTo,
value: value.toString(),
validAfter: "0",
validBefore: validBefore.toString(),
nonce,
};
const proof = { x402Version: 1, scheme: "exact", network: accepts.network, payload: { signature, authorization } };
const paid = await fetch(url, { headers: { "X-PAYMENT": Buffer.from(JSON.stringify(proof)).toString("base64") } });
if (!paid.ok) throw new Error(`x402 read failed: ${paid.status}`);
const raw = paid.headers.get("x-payment-response");
return { body: await paid.json(), receipt: raw ? JSON.parse(Buffer.from(raw, "base64").toString()) : null };
}Or buy through Virtuals ACP
A Virtuals agent can buy the same reads as ACP jobs instead of signing x402 itself. The public offerings are fixed-fee and deliver the same JSON:
| Offering | Fee | Delivers |
|---|---|---|
gm_omega_book | $0.10 | One cell, as GET /v1/gm/conviction |
gm_market_line | $0.10 | The omega market line, as GET /v1/gm/market_line |
Offering specs and enums are in https://gm.mutantdefi.com/services.json under acp_offering_specs.
The storefront lists exactly these two reads. Pay-to is the same address as x402 (0x430c…8059) and fund_transfer stays off. A token sink, when it ships, locks bought tokens; it does not burn them. Quiet 402s (lattice, weights, wallet book) are not storefront offerings.
Reading the policy's cell through ACP
- Buy gm_omega_book with exactly {"symbol": "BTC4H/DFY"}, then ETH4H/DFY and HYPE4H/DFY. That returns the conviction cell the policy gates on.
- Never add universe, mode, notional or max_weight to gm_omega_book. Those return a wallet book: portfolio weights, not a signal.
- gm_market_line is context. It never opens, sizes or closes a lot.
- ACP serves the live cell but not its history. Over ACP alone, build the band series from your own hourly reads and start trading after 168 hours. With a key or x402, read conviction_history at start and the band exists on day one.
- Read the 4H cell once an hour. The 5-minute, 15M and 90M cells are Kraken Futures prints, not Hyperliquid, and the 1D cell is not the basis: none of them is an input to this trader.
- staleness_ms is the age of the last bar's open, so a healthy 1h cell reads 1 to 2 hours old and the gate is 3 hours. Do not hold a 1h cell to a 5-minute freshness gate.
- The band is each cell's own 87.5th percentile of |signed_conviction| over 720 hours, not a fixed threshold such as 0.14. Confidence is logged, not gated.
- Size comes only from the gain-mass rank (3, 5 or 8 lots). Portfolio weights and size multipliers from a book are not inputs.
- A cell under its band is no signal. Report the gates, not a lean.
Reading the job room
- System entries (
kind=system): readevent.typeandevent.*(job.created,budget.set,job.funded,job.submitted, …). - Requirement messages (
kind=message): readcontentTypeandcontent. Never readeventon a message; it is empty andcontentholds the JSON. - Lifecycle: create-job → requirement message →
budget.set→job.funded→job.submitted(deliverable) → completed, rejected or expired. - Accept deliverables whose
acp_offering_idisgm_omega_bookorgm_market_line.
Instruments
Every cell is addressed GM! / VENUE / INSTRUMENT / HORIZON and names the venue it observes. GM!/HYPERLIQUID is built from Hyperliquid prices for Hyperliquid traders:
| Series | Book | Bar | Horizon | Status |
|---|---|---|---|---|
| GM!/HYPERLIQUID | Core-6 | 1h | 1D | production |
| GM!/HYPERLIQUID | Core-6 · opposite-sign oscillator | 1h | 4H | production |
| GM!/HYPERLIQUID | Crypto-3 | 1h | 8H | experimental |
The published GM!/HYPERLIQUID cells:
| Cell | Series | Horizon |
|---|---|---|
BTC1D/DFY | GM!/HYPERLIQUID | 1D |
ETH1D/DFY | GM!/HYPERLIQUID | 1D |
HYPE1D/DFY | GM!/HYPERLIQUID | 1D |
XYZ-GOLD1D/DFY | GM!/HYPERLIQUID | 1D |
XYZ-SILVER1D/DFY | GM!/HYPERLIQUID | 1D |
XYZ-CL1D/DFY | GM!/HYPERLIQUID | 1D |
BTC4H/DFY | GM!/HYPERLIQUID | 4H |
ETH4H/DFY | GM!/HYPERLIQUID | 4H |
HYPE4H/DFY | GM!/HYPERLIQUID | 4H |
GM!/HYPERLIQUID 8H is DataFi's experimental series. It is not served. A trader switches to it only when a read returns it with methodology_status: "production". DataFi also prints GM!/KRAKEN_FUTURES series for Kraken Futures traders. A Virtuals trader on Hyperliquid does not read them.
The cell
GET /v1/gm/conviction?symbol=BTC4H/DFY returns one cell. The fields a trader uses:
| Field | Meaning |
|---|---|
symbol | Published name, e.g. BTC1D/DFY. |
index_address | GM! / VENUE / INSTRUMENT / HORIZON, e.g. GM!/HYPERLIQUID/BTC/4H. |
source_venue | hyperliquid or krakenfutures. The venue the cell observes. |
methodology_status | production or experimental. Trade production only. |
signed_conviction | Signed index level in [−1, 1]. The admit is ranked on this. |
confidence | Cell confidence in [0, 1]. Logged, not gated. |
value_mode | composite on the production core6 cells; omega when signed_conviction is itself the omega direction. |
omega_direction | 2·G/(G+L) − 1 at the cell's hurdle: a read of the recent move. The hurdle-net side, except on an opposite-sign book, where it points against the conviction by construction. |
omega_hurdle | The hurdle θ the cell's omega is netted at: 0 on the core6 cells today. Your τ is applied on your side. |
observation_ts | Last underlying bar the cell saw. |
publication_ts | When the gateway served the cell. |
staleness_ms | Age of the last closed bar's open, normally 1–2 h. Above 3 h, refuse new entries. |
params_hash | Profile parameters. A change means re-reading the cell's history and replacing your band series. |
band_version | The cell's band calibration stamp. |
Timestamps are Unix milliseconds. The gateway caches a cell for 300 seconds and the bar is hourly, so a second read inside the hour buys the same cell.
Reads and budget
| Read | When | Price | Required | Use |
|---|---|---|---|---|
/v1/gm/conviction?symbol={BASE}4H/DFY | Every hour, each instrument, after :05 UTC | $0.10 | yes | The basis cell: signed conviction, hurdle-net omega, venue, address, timestamps. |
/v1/gm/conviction_history?symbol={BASE}4H/DFY&limit=720 | At start, and after any params_hash change | $0.10 | yes | The cell's own hourly record, oldest first, ending at the live cell. The band ranks it, so it exists on day one. Free with an API key. |
/v1/feed/metadata | At start and every 24 h | free | yes | Feed version, params hash, and the venue series block. A changed params hash means re-reading the cell's history. The 4H book's support (opposite_sign_oscillator) decides gate 2. |
/.well-known/x402 | At start and on any unexpected 402 | free | yes | Prices, network, asset and payTo for the public routes. |
/v1/gm/market_line?universe=core6&horizon=4H | Once an hour | $0.10 | no | The omega market line for the book, for the record. |
/v1/gm/hazard?symbol={BASE}4H/DFY&side={side}&entry_ts={unix} | Each hour a lot is open | $0.10 | no | DataFi's exit hazard for the lot you describe. Advisory: logged, never an exit. |
Over x402, three instruments read hourly cost 72 reads, $7.20 a day. The market line adds $2.40 a day; hazard adds $0.10 for each hour a lot is open. The history read is $0.30 at start for three instruments. With an API key the same schedule costs nothing.
The band's series is the cell's own record. conviction_history returns records oldest first (ts, signed_conviction, confidence, omega_direction) with the cell's venue, address, methodology and params_hash, ending at the live cell's bar. Each record is what the cell printed at that bar, from data up to that bar only. The band therefore exists on day one. Refuse a history whose venue, address, methodology or params_hash does not match the live cell. Over ACP you can buy the live cell but not its history, so an ACP-only agent builds the series itself and waits 168 hours. The weights behind the cell were fit on a historical window, so older records are not out of sample with respect to those parameters: paper confirmation is still the test.
Trader policy
One lot per instrument, at most 4 hours, entered as a maker only when the index is in its own tail and the expected move clears Hyperliquid fees and funding. The reference implementation is gm_occupancy.py.
What is validated on Hyperliquid
The fee, funding and order mechanics are Hyperliquid's. The selection and exit numbers are provisional: a starting point carried from a reference book, not yet confirmed on Hyperliquid. A Hyperliquid paper record that passes the confirmation law is what validates them.
| Parameter | Value | State |
|---|---|---|
| Hurdle τ | maker + taker + 2 × builder, your own rates | Hyperliquid |
| Time value ρ | r_f / 8760 per hour on notional, r_f default 4.5% a year | declared |
| Funding in the profit gate | side × hourly rate × 4 | Hyperliquid |
| Entry order | post-only (Alo) at the touch | Hyperliquid |
| Band | 87.5th percentile of |conviction|, 720 h window, 168 h minimum | provisional |
| Profit premium π | π = τ_H, so h* = 2 τ_H | provisional |
| Hold | 4 h, the basis cell's horizon | provisional |
| Floor | k = 6, L = 12, move-against 12 τ_b | provisional |
| Floor unit τ_b | clamp(2.5 · LM / 12, 0.3 τ, 3 τ) | provisional, τ-scaled |
| Size | 3 / 5 / 8 lots by gain-mass thirds | provisional |
| Kill | −10 R, R = lot net ÷ (12 τ_b × notional); live: 2 × paper drawdown, at least 10 R | provisional, loss-mass scaled |
| Confirmation | 60 / 120 / 180 lots, P ≥ 0.80 confirm, P ≤ 0.10 revert | venue-neutral law |
Hyperliquid 1h candles, March to October 2026. These diagnostics use price only; they do not score the cell's own history:
- At the base-tier τ of 6 bps, the clamp's upper bound (3τ = 18 bps) binds on HYPE in about one hour in six, so HYPE's floor is then tighter than its loss mass would set. BTC (τ_b ≈ 5.5 bps) and ETH (≈ 7 bps) sit inside the clamp.
- h* ≈ 12.4 bps (fees 12, time value 0.4 at 4.5%) is small next to a median 4-hour move of 36 bps (BTC), 46 (ETH) and 89 (HYPE): on Hyperliquid the band does most of the selecting.
- The floor is at or above the move-against line for the whole hold, so the floor fires first; move-against is a backstop.
Rerun them at your own rates with hl_diagnostics.py (next to gm_occupancy.py): python3 hl_diagnostics.py --maker 0.00012 --taker 0.0004.
Hurdle τ
τ = r_add + r_cross + 2 · b: a maker entry, a taker exit, and the builder fee b per fill if you trade through a builder code. Read your own rates from userFees; staking and referral discounts lower them. Hyperliquid has no fixed fee per order, so τ does not depend on lot size. It charges every exit as taker, so it is a ceiling on fees.
The gates add the time value of the notional over the hold: τ_H = τ + ρ · 4 with ρ = r_f / 8760 per hour and r_f a risk-free rate you declare (default 4.5% a year). At the default that is 0.21 bps over 4 hours, so τ_H = 6.21 bps and h* = 2 τ_H = 12.41 bps at base tier. Funding is charged separately; the floor clamp and the re-entry hold use the fee τ.
| 14-day volume tier | Maker | Taker | τ (no builder) |
|---|---|---|---|
| Base | 0.015% | 0.045% | 0.060% |
| > $5M 14-day volume | 0.012% | 0.040% | 0.052% |
| > $25M 14-day volume | 0.008% | 0.035% | 0.043% |
Entry
Once an hour, on the completed bar, when flat in the instrument. All five must pass.
| Gate | Rule | State |
|---|---|---|
| Band | The 87.5th percentile of |signed_conviction| over the trailing 720 hours, excluding this hour, with at least 168. Long at or above the band, short at or below its negative. The series is seeded at start from the cell's own history (conviction_history) and extended by each hourly read. No trade under 168 hours. | provisional |
| Hurdle-net | The cell's omega, netted at its omega_hurdle, points to your side (omega_direction on composite cells). On an opposite-sign book (the 4H basis: support opposite_sign_oscillator in /v1/feed/metadata) this part is skipped: the conviction fades the move omega reads, so omega points the other way by construction and is not same-side support. Your own Ω at τ_H over the taken sample is logged; it exceeds 1 exactly when E[R | taken] > τ_H, so the profit bar implies it. | structural |
| Profit-net | E[R | taken] − f₄ > τ_H + π with π = τ_H, so the bar is h* = 2 τ_H, where τ_H = τ + ρ·4 adds the time value of the notional at your declared r_f. f₄ is the funding your side pays over 4 hours at the current hourly rate. E[R | taken] is the mean side-signed 4h log return over trailing hours the band admitted on this side, at least 20 of them. | Hyperliquid fees and funding, declared r_f; π provisional |
| Blocks | Not while a same-side stay-out holds, while mid is within τ of the last flatten, after the kill, or when the hour's cell was refused. | structural |
| Size | Rank the trade-side gain mass (G0 long, L0 short, of 4h log returns over 720 hours) against its prior 720 hours: thirds give 3, 5 or 8 lots; unranked gives 5. Round down to szDecimals; skip under the $10 minimum order value. | provisional |
There is no profit take: the profit bar chooses the lot; the floor and the clock close it. A lot is LOT_USD of notional (default $100).
Maker entry
- Post-only limit (tif "Alo") at your side's touch: best bid for a long, best ask for a short.
- Work it up to 15 minutes, re-posting at the new touch. Skip the hour once mid has moved more than τ your way from the decision mid: do not chase.
- An Alo order that would cross is rejected. Re-read the book and re-post; never convert it to a taker order.
- A partial fill is the lot: cancel the rest and stamp the filled size.
- Isolated margin, with the liquidation price beyond 24 τ_b from entry.
Exit
Stamp the floor unit at entry: τ_b = clamp(2.5 · LM / 12, 0.3τ, 3τ), with LM = (L0 + G0) / 2 of 4h log returns over 720 hours. The clamp is in units of τ, so a cheaper fee tier tightens it; log whether it bound. Flatten at the first of:
| Exit | Rule |
|---|---|
| Floor | Long when mid ≤ entry·(1 + floor(t)); short when mid ≥ entry·(1 − floor(t)). floor(t) = (6·τ_b/4)·min(t, 4) − 12·τ_b. |
| Move-against | Long when mid ≤ entry·(1 − 12·τ_b); short when mid ≥ entry·(1 + 12·τ_b). |
| Time | The lot is 4 hours old. Winners and losers alike. |
- Floor and move-against are taker: keep a reduce-only stop-market trigger (tpsl "sl") at the current floor, moved up at least every 15 minutes, and close reduce-only IOC if a minute check finds the floor crossed first.
- Time is maker first: at 4 hours post a reduce-only Alo at the touch for up to 5 minutes, then close the rest reduce-only IOC.
The floor starts at −12 τ_b and rises to −6 τ_b at 4 hours. No trail, no take. A stale read never closes a lot, and a band flip waits until you are flat. After a floor or move-against stop, the same side stays out until mid trades back through that entry; after any flatten, wait while mid is within τ of the flatten price.
SOUL.md
SOUL.md is the target trader: identity, the parameter states, universe and basis, the x402 and Hyperliquid reads, the fields it refuses on, τ, the five entry gates, maker entry, the exits and how to execute them, the kill and the confirmation law, the record, and what it never does. Give it to your agent as its operating instructions: the character or goal text on Virtuals, or the system prompt of the runtime that drives the agent. On Virtuals, use VIRTUALS_SOUL.md: the same policy with the acp trade calls, the schedule, the ACP read rules, the hourly status report and the DegenClaw posting rules. Load one file, not both, and replace any older instructions entirely: a leftover loop or threshold from a previous strategy will keep running.
- The policy is in the file, not in the model. The agent applies it with gm_occupancy.py or your own port of it.
- Change a provisional number and you have a new version: the kill and the confirmation count restart. Never tune on the record you are confirming with.
Reference loop
Hyperliquid's info endpoint is free. The band ranks the cell's own signed_conviction, not price: read /v1/gm/conviction_history at start (and when params_hash changes), pair each record to the Hyperliquid bar that opens at its ts, and extend it with every hourly read. A bar with no record is a gap, not a zero. The venue calls (venue_mid, hl_funding_hourly, work_alo_entry, place_floor_trigger, move_floor_trigger, maker_then_ioc, reduce_only_ioc, now_s) are yours; in paper they simulate fills at the touch.
| Info request | Use |
|---|---|
candleSnapshot | 1h closes for the taken returns, gain mass and loss mass. |
l2Book | The touch for post-only entries and maker time exits. |
allMids | Mid for the floor check, at least once a minute. |
metaAndAssetCtxs | Hourly funding, szDecimals and max leverage. |
userFees | Your userAddRate and userCrossRate, after discounts. Re-read every 24 h. |
clearinghouseState, userFills | Position, margin and fills for the record. |
"""Free 1h bars from Hyperliquid's info endpoint, oldest first: open time (ms) and close."""
import time
import httpx
def hl_bars(coin: str, hours: int = 1500) -> list[dict]:
end = int(time.time() * 1000)
body = {
"type": "candleSnapshot",
"req": {"coin": coin, "interval": "1h", "startTime": end - hours * 3_600_000, "endTime": end},
}
bars = httpx.post("https://api.hyperliquid.xyz/info", json=body, timeout=30).json()
closed = [b for b in bars if int(b["T"]) < end] # drop the bar still forming
return [{"t": int(b["t"]), "c": float(b["c"])} for b in closed]"""One hourly decision per instrument, paper first. The venue calls are yours."""
import math, os
import httpx
import gm_occupancy as g # /trader/gm_occupancy.py
GM = "https://gm.mutantdefi.com"
FEES = g.FEES["hyperliquid_base"] # replace with your userFees rates
BUILDER = 0.0 # builder fee per fill, if you trade through one
LOT_USD = 100
RHO = g.rho(0.045) # declared r_f: time value per hour on notional
API_KEY = os.environ.get("GM_API_KEY") # DataFi's own trader: no 402, no receipt
def opposite_sign_basis() -> bool:
"""Read at start and every 24 h: is the 4H basis book opposite-sign support?"""
meta = httpx.get(f"{GM}/v1/feed/metadata", timeout=30).json()
return g.book_support(meta, g.BASIS_HORIZON) == g.OPPOSITE_SIGN
def read_cell(base: str) -> tuple[dict, dict | None, str]:
url, params = f"{GM}/v1/gm/conviction", {"symbol": f"{base}4H/DFY"}
if API_KEY:
r = httpx.get(url, params=params, headers={"x-api-key": API_KEY}, timeout=30)
r.raise_for_status()
return r.json(), None, "api_key"
cell, receipt = paid_get(url, params)
return cell, receipt, "x402"
def seed_band_series(base: str, state: dict) -> None:
"""At start, and again when params_hash changes: the cell's own record, keyed by bar open time."""
url, params = f"{GM}/v1/gm/conviction_history", {"symbol": f"{base}4H/DFY", "limit": 720}
if API_KEY:
r = httpx.get(url, params=params, headers={"x-api-key": API_KEY}, timeout=60)
r.raise_for_status()
hist = r.json()
else:
hist, _receipt = paid_get(url, params)
refused = g.history_usable(hist, basis_address=f"GM!/HYPERLIQUID/{base}/4H",
live_params_hash=state.get("params_hash"))
if refused:
raise RuntimeError(f"history refused: {refused}")
state["by_ts"] = {r["ts"]: r["signed_conviction"] for r in hist["records"]
if r["signed_conviction"] is not None}
state["params_hash"] = hist["params_hash"]
state["band_source"] = {"source": "history", "n": hist["n"], "params_hash": hist["params_hash"]}
state["opposite_sign"] = opposite_sign_basis() # also refresh every 24 h
def hourly(base: str, state: dict) -> dict:
"""Run once per closed 1h bar, after :05 UTC. state persists across hours."""
try:
cell, receipt, access = read_cell(base)
if "by_ts" not in state or cell["params_hash"] != state.get("params_hash"):
seed_band_series(base, state) # day-one band; a new hash replaces the series
except Exception as exc: # a failed read blocks entries, never exits
return {"base": base, "read_failed": str(exc)}
bars = hl_bars(base) # oldest first, closed bars only: {"t", "c"}
closes = [b["c"] for b in bars]
mid = venue_mid(base)
tau = g.tau(**FEES, builder=BUILDER)
funding = hl_funding_hourly(base) # metaAndAssetCtxs, current hourly rate
signed = float(cell["signed_conviction"])
state["by_ts"][cell["observation_ts"]] = signed
history = g.seed_signed([{"ts": t, "signed_conviction": v} for t, v in state["by_ts"].items()],
[b["t"] for b in bars]) # paired to Hyperliquid bars by open time; NaN gaps
level = g.band(history[:-1]) # the band excludes this hour
tau_h = g.tau_hold(tau, RHO)
row = {"base": base, "cell": cell, "access": access, "receipt": receipt, "mid": mid,
"tau": tau, "rho": RHO, "tau_h": tau_h, "funding_hourly": funding, "band": level}
if state.get("lot"): # exits also run every minute: see watch()
return row
refused = g.cell_usable(cell, basis_address=f"GM!/HYPERLIQUID/{base}/4H")
if refused:
return {**row, "refused": refused}
side = g.admit(signed, level)
opposite = state["opposite_sign"]
if side is None or not g.hurdle_net(side, cell, opposite_sign=opposite):
return {**row, "side": side, "hurdle_net": side is not None and g.hurdle_net(side, cell, opposite_sign=opposite)}
taken = g.taken_returns(closes, history, side)
e_taken = sum(taken) / len(taken) if len(taken) >= g.MIN_TAKEN else None
failed_side, failed_entry = state.get("failed") or (None, None)
blocked = (
g.stay_out(side, mid, failed_side, failed_entry)
or g.reentry_hold(mid, state.get("flatten_px"), tau)
or g.killed(state.get("closed_r", []), state.get("kill_r", g.KILL_R))
)
row.update(side=side, e_taken=e_taken, n_taken=len(taken), f4=g.funding_cost(side, funding),
omega_tau_h=g.omega_at(taken, tau_h), h_star=g.profit_bar(tau, RHO), blocked=blocked)
if not g.profit_net(e_taken, tau, side, funding, RHO) or blocked:
return row
n_lots = g.lots(g.gain_mass_rank(closes, side))
fill = work_alo_entry(base, side, n_lots * LOT_USD / mid, decision_mid=mid, tau=tau) # maker or nothing
if fill:
tau_b = g.tau_b(closes, tau)
state["lot"] = {"side": side, "entry": fill["px"], "open_s": now_s(), "tau_b": tau_b,
"lots": n_lots, "notional": fill["sz"] * fill["px"]}
place_floor_trigger(base, state["lot"], g.floor_return(0, tau_b))
return {**row, "lots": n_lots, "entered": bool(fill)}
def watch(base: str, state: dict) -> str | None:
"""Run at least once a minute while a lot is open."""
lot = state.get("lot")
if not lot:
return None
mid = venue_mid(base)
age = (now_s() - lot["open_s"]) / 3600
move_floor_trigger(base, lot, g.floor_return(age, lot["tau_b"])) # every 15 min is enough
reason = g.exit_reason(lot["side"], lot["entry"], mid, age, lot["tau_b"])
if reason:
fill = maker_then_ioc(base, lot) if reason == "time" else reduce_only_ioc(base, lot)
state["closed_r"] = state.get("closed_r", []) + [g.lot_r(fill["usd"], lot["notional"], lot["tau_b"])]
state.update(lot=None, flatten_px=fill["px"],
failed=(lot["side"], lot["entry"]) if reason != "time" else None)
return reasonExecute on Hyperliquid
Two ways to place orders. On Virtuals, use acp trade from Run on Virtuals. Outside it, or if you want a resting stop, use the Hyperliquid SDK below with an approved API wallet.
- Trade from an API wallet approved for your account, so the agent key cannot withdraw. Isolated margin, one position per instrument; never add to an open lot.
- Entries are
Alo(post-only) at the touch, never taker. The floor rides as a reduce-only stop-market trigger; the time exit tries a reduce-onlyAlofirst, thenIoc. - Round sizes down to the perp's
szDecimals; Hyperliquid rejects orders under $10 of value. - The universe is BTC, ETH, HYPE on the GM!/HYPERLIQUID 4H cells, read hourly; the 4-hour hold is the cell's horizon. The 4H book is opposite-sign support, so the cell's omega direction is not asked to agree with the side (gate 2).
"""Hyperliquid orders for the policy (hyperliquid-python-sdk). Paper: log instead of send."""
import os
from eth_account import Account
from hyperliquid.exchange import Exchange
from hyperliquid.utils import constants
wallet = Account.from_key(os.environ["HL_AGENT_KEY"]) # an API wallet, never a prompt
ex = Exchange(wallet, constants.MAINNET_API_URL, account_address=os.environ["HL_ACCOUNT"])
BUILDER = None # e.g. {"b": "0x…", "f": 10}: f is tenths of a bp per fill; add it to τ
def alo_entry(coin: str, is_buy: bool, sz: float, touch: float):
"""Post-only at the touch. An Alo that would cross is rejected, not filled."""
return ex.order(coin, is_buy, sz, touch, {"limit": {"tif": "Alo"}}, builder=BUILDER)
def floor_trigger(coin: str, is_long: bool, sz: float, floor_px: float):
"""Reduce-only stop-market at the floor; re-place it as the floor rises."""
order_type = {"trigger": {"triggerPx": floor_px, "isMarket": True, "tpsl": "sl"}}
return ex.order(coin, not is_long, sz, floor_px, order_type, reduce_only=True, builder=BUILDER)
def reduce_only_ioc(coin: str, is_long: bool, sz: float, limit_px: float):
"""Taker close; limit_px a few ticks through the touch caps slippage."""
return ex.order(coin, not is_long, sz, limit_px, {"limit": {"tif": "Ioc"}}, reduce_only=True, builder=BUILDER)Record and promotion
One line per decision, entered or not. The record is the only evidence the file works for you.
{
"ts": "2026-10-02T14:05:12Z",
"instrument": "BTC",
"cell": {
"index_address": "GM!/HYPERLIQUID/BTC/4H",
"signed_conviction": 0.512,
"confidence": 0.63,
"value_mode": "composite",
"omega_direction": -0.187,
"omega_hurdle": 0.0,
"observation_ts": 1790971200000,
"publication_ts": 1790978710000,
"staleness_ms": 7510000,
"params_hash": "…"
},
"access": "x402",
"receipt": { "transaction": "0x…", "network": "base" },
"band": 0.471,
"band_series": { "source": "history", "n": 720, "params_hash": "…" },
"gates": { "band": "long", "hurdle_net": "skipped: opposite_sign_oscillator", "profit_net": true, "blocked": false },
"fees": { "maker": 0.00015, "taker": 0.00045, "builder": 0.0 },
"tau": 0.0006, "rf_apr": 0.045, "rho": 0.00000514, "tau_h": 0.000621, "h_star": 0.001241,
"funding_hourly": 0.0000125, "f4": 0.00005,
"e_taken": 0.0021, "n_taken": 34, "omega_tau_h": 1.38,
"gain_mass_rank": 0.71, "lots": 8,
"venue": { "venue": "hyperliquid", "mid": 84492.5, "spread_bps": 0.9, "position": 0, "margin": "isolated" },
"entry": { "order": "Alo", "minutes_worked": 3, "filled_sz": 0.00946, "maker": true },
"action": "enter_maker",
"mode": "paper"
}- Kill. Count each closed lot in R: its net P&L after fees and funding, divided by its own initial floor risk, 12 τ_b of notional. τ_b comes from the instrument's loss mass, so one R is a full stop on BTC, ETH or HYPE alike. When the running sum falls below −10 R, stop opening lots. Live re-stamps the kill at 2× the confirmed paper record's drawdown in R, never under 10 R. Open lots keep their exits. The kill latches until a new version.
- Confirm. At 60, 120, 180 closed lots, take the mean net bps and a day-clustered bootstrap of P(mean > 0) (1,000 draws). Confirm when the mean is above zero and P ≥ 0.80; revert when P ≤ 0.10, or at 180 without confirmation.
- Paper runs on Hyperliquid prices, your fee tier and real funding until it confirms. Live starts again at lot 0 under the same law.
Endpoint reference
| Route | Parameters | Price | Note |
|---|---|---|---|
GET /v1/gm/conviction | symbol, or base + horizon | $0.10 | One cell. First snapshot free per payer. |
GET /v1/gm/conviction_history | symbol, limit (default 720, max 2000), since | $0.10 | The cell's own hourly record: signed_conviction, confidence, omega_direction. Causal. The band's input. |
GET /v1/gm/market_line | universe, horizon | $0.10 | Omega market line for a book. |
GET /v1/gm/comparison | none | $0.10 | Cross-book comparison. |
GET /v1/gm/lattice | universe, horizon, granularity | $0.10 | Every cell in a universe and horizon. |
GET /v1/gm/weights | universe, horizon, weighting | $0.12 | Stake multipliers in [0.50, 1.50]. |
GET /v1/gm/hazard | symbol, side, entry_ts, mae?, current_profit? | $0.10 | Exit hazard for a position you supply. |
GET /v1/gm/ohlcv/{symbol} | timeframe, limit, since | $0.08 | Bars behind a cell. |
POST /mcp/execute | { name, args } | $0.10 | The same reads as MCP tools. |
GET /.well-known/x402 | none | free | Public paid routes and settlement terms. |
GET /services.json | none | free | Machine catalog and ACP offering specs. |
GET /v1/feed/metadata | none | free | Feed version, params hash, venue series. |
GET /mcp/tools | none | free | MCP tool registry. |
GET /health | none | free | Liveness and upstream feed status. |
The trader never:
- Trade without a confirmed paper record.
- Trade a cell whose methodology_status is not production.
- Trade a cell that is not GM!/HYPERLIQUID, or trade anywhere but Hyperliquid.
- Enter with a taker order.
- Invent a cell, a price or a fill.
- Put a private key in a prompt, a log or a reply.
- Present the file, a cell or your record as advice or an offer.
Guides and reference code only. MutantDeFi publishes no index, takes no custody and executes nothing. Not investment advice, not an offer.