Developer docs

Build a Virtuals trader on GM!

Everything an agent needs to read DataFi's GM! Index over x402, an API key or ACP and trade Hyperliquid perpetuals from its own Virtuals wallet: setup, payment, the cell, a Hyperliquid policy with maker entries, the SOUL.md that carries it, and a reference loop.

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 to https://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

StepCommandNote
Check the CLI and its skillacp skill check --json # prefer the bundled SKILL.md (acp skill print) over any cached copyacp-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> --jsonThe human only clicks links. Your agent runs every command itself.
Create the agent and its signeracp agent create acp agent add-signerThe signer approves on a URL, once. Trades sign with it. Probe for NO_SIGNER before re-running.
Fund the Hyperliquid accountacp trade --token-in usdc --chain-in 8453 --amount-in 100 --token-out usdc --chain-out 1337 --dry-run --jsonHyperliquid 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:

ActionCallNote
Maker entryacp trade --side long --token BTC --size <sz> --price <touch> --post-only --isolated --leverage <L> --jsonBest 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 exitacp trade --side short --token BTC --size <open> --reduce-only --slippage <max> --jsonClose with the opposite side and --reduce-only. Market, taker.
Time exitacp trade --side short --token BTC --size <open> --price <touch> --reduce-only --post-only --jsonMaker first for up to 5 minutes, then the market close above.
Position, marginacp trade hl-status --jsonReconcile it against your record on every start.
Paperadd --dry-run to any order abovePaper means the same call, with --dry-run, against live Hyperliquid prices and funding.

Schedule

WhenWhat
Every minute, while a lot is openRead mid. Check the floor, move-against and the 4-hour clock. Close as above.
Every hour, after :05 UTCRefresh 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 hoursRe-read userFees and /v1/feed/metadata.
On startRead 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 backEscalate 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.

shell
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-REQUIRED

Pay with x402

Settlement is USDC on Base (chain 8453), asset 0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913, payTo 0x430c298f9a4a145e5299af997287bb4f928b8059. The facilitator settles, and pays the gas.

  1. Request the resource. A 402 carries the challenge twice: accepts[] in the body (v1) and the base64 PAYMENT-REQUIRED header (v2).
  2. Sign the EIP-3009 TransferWithAuthorization it asks for: domain USD Coin / 2, value maxAmountRequired, a random 32-byte nonce, and a short validBefore.
  3. Resend the same request with the base64 proof in X-PAYMENT (v1) or PAYMENT-SIGNATURE (v2).
  4. Keep the receipt in X-PAYMENT-RESPONSE (v1) or PAYMENT-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.

python · eth-account, httpx
"""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"])
typescript · viem
// 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:

OfferingFeeDelivers
gm_omega_book$0.10One cell, as GET /v1/gm/conviction
gm_market_line$0.10The 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): read event.type and event.* (job.created, budget.set, job.funded, job.submitted, …).
  • Requirement messages (kind=message): read contentType and content. Never read event on a message; it is empty and content holds the JSON.
  • Lifecycle: create-job → requirement message → budget.set → job.funded → job.submitted (deliverable) → completed, rejected or expired.
  • Accept deliverables whose acp_offering_id is gm_omega_book or gm_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:

SeriesBookBarHorizonStatus
GM!/HYPERLIQUIDCore-61h1Dproduction
GM!/HYPERLIQUIDCore-6 · opposite-sign oscillator1h4Hproduction
GM!/HYPERLIQUIDCrypto-31h8Hexperimental

The published GM!/HYPERLIQUID cells:

CellSeriesHorizon
BTC1D/DFYGM!/HYPERLIQUID1D
ETH1D/DFYGM!/HYPERLIQUID1D
HYPE1D/DFYGM!/HYPERLIQUID1D
XYZ-GOLD1D/DFYGM!/HYPERLIQUID1D
XYZ-SILVER1D/DFYGM!/HYPERLIQUID1D
XYZ-CL1D/DFYGM!/HYPERLIQUID1D
BTC4H/DFYGM!/HYPERLIQUID4H
ETH4H/DFYGM!/HYPERLIQUID4H
HYPE4H/DFYGM!/HYPERLIQUID4H

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:

FieldMeaning
symbolPublished name, e.g. BTC1D/DFY.
index_addressGM! / VENUE / INSTRUMENT / HORIZON, e.g. GM!/HYPERLIQUID/BTC/4H.
source_venuehyperliquid or krakenfutures. The venue the cell observes.
methodology_statusproduction or experimental. Trade production only.
signed_convictionSigned index level in [−1, 1]. The admit is ranked on this.
confidenceCell confidence in [0, 1]. Logged, not gated.
value_modecomposite on the production core6 cells; omega when signed_conviction is itself the omega direction.
omega_direction2·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_hurdleThe hurdle θ the cell's omega is netted at: 0 on the core6 cells today. Your τ is applied on your side.
observation_tsLast underlying bar the cell saw.
publication_tsWhen the gateway served the cell.
staleness_msAge of the last closed bar's open, normally 1–2 h. Above 3 h, refuse new entries.
params_hashProfile parameters. A change means re-reading the cell's history and replacing your band series.
band_versionThe 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

ReadWhenPriceRequiredUse
/v1/gm/conviction?symbol={BASE}4H/DFYEvery hour, each instrument, after :05 UTC$0.10yesThe basis cell: signed conviction, hurdle-net omega, venue, address, timestamps.
/v1/gm/conviction_history?symbol={BASE}4H/DFY&limit=720At start, and after any params_hash change$0.10yesThe 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/metadataAt start and every 24 hfreeyesFeed 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/x402At start and on any unexpected 402freeyesPrices, network, asset and payTo for the public routes.
/v1/gm/market_line?universe=core6&horizon=4HOnce an hour$0.10noThe 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.10noDataFi'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.

ParameterValueState
Hurdle τmaker + taker + 2 × builder, your own ratesHyperliquid
Time value ρr_f / 8760 per hour on notional, r_f default 4.5% a yeardeclared
Funding in the profit gateside × hourly rate × 4Hyperliquid
Entry orderpost-only (Alo) at the touchHyperliquid
Band87.5th percentile of |conviction|, 720 h window, 168 h minimumprovisional
Profit premium ππ = τ_H, so h* = 2 τ_Hprovisional
Hold4 h, the basis cell's horizonprovisional
Floork = 6, L = 12, move-against 12 τ_bprovisional
Floor unit τ_bclamp(2.5 · LM / 12, 0.3 τ, 3 τ)provisional, τ-scaled
Size3 / 5 / 8 lots by gain-mass thirdsprovisional
Kill−10 R, R = lot net ÷ (12 τ_b × notional); live: 2 × paper drawdown, at least 10 Rprovisional, loss-mass scaled
Confirmation60 / 120 / 180 lots, P ≥ 0.80 confirm, P ≤ 0.10 revertvenue-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 tierMakerTakerτ (no builder)
Base0.015%0.045%0.060%
> $5M 14-day volume0.012%0.040%0.052%
> $25M 14-day volume0.008%0.035%0.043%

Entry

Once an hour, on the completed bar, when flat in the instrument. All five must pass.

GateRuleState
BandThe 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-netThe 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-netE[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
BlocksNot 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
SizeRank 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:

ExitRule
FloorLong when mid ≤ entry·(1 + floor(t)); short when mid ≥ entry·(1 − floor(t)). floor(t) = (6·τ_b/4)·min(t, 4) − 12·τ_b.
Move-againstLong when mid ≤ entry·(1 − 12·τ_b); short when mid ≥ entry·(1 + 12·τ_b).
TimeThe 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 requestUse
candleSnapshot1h closes for the taken returns, gain mass and loss mass.
l2BookThe touch for post-only entries and maker time exits.
allMidsMid for the floor check, at least once a minute.
metaAndAssetCtxsHourly funding, szDecimals and max leverage.
userFeesYour userAddRate and userCrossRate, after discounts. Re-read every 24 h.
clearinghouseState, userFillsPosition, margin and fills for the record.
python · Hyperliquid 1h closes
"""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]
python · hourly decision and minute watch
"""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 reason

Execute 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-only Alo first, then Ioc.
  • 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).
python · hyperliquid-python-sdk
"""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.

json · one decision
{
  "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

RouteParametersPriceNote
GET /v1/gm/convictionsymbol, or base + horizon$0.10One cell. First snapshot free per payer.
GET /v1/gm/conviction_historysymbol, limit (default 720, max 2000), since$0.10The cell's own hourly record: signed_conviction, confidence, omega_direction. Causal. The band's input.
GET /v1/gm/market_lineuniverse, horizon$0.10Omega market line for a book.
GET /v1/gm/comparisonnone$0.10Cross-book comparison.
GET /v1/gm/latticeuniverse, horizon, granularity$0.10Every cell in a universe and horizon.
GET /v1/gm/weightsuniverse, horizon, weighting$0.12Stake multipliers in [0.50, 1.50].
GET /v1/gm/hazardsymbol, side, entry_ts, mae?, current_profit?$0.10Exit hazard for a position you supply.
GET /v1/gm/ohlcv/{symbol}timeframe, limit, since$0.08Bars behind a cell.
POST /mcp/execute{ name, args }$0.10The same reads as MCP tools.
GET /.well-known/x402nonefreePublic paid routes and settlement terms.
GET /services.jsonnonefreeMachine catalog and ACP offering specs.
GET /v1/feed/metadatanonefreeFeed version, params hash, venue series.
GET /mcp/toolsnonefreeMCP tool registry.
GET /healthnonefreeLiveness 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.