Follow a signal#
The follower's walk for door 3: find an operator, read the record, give your mandate the signal feed, then plan, order and report each signal.
Testnet only. The mandate vault is Base Sepolia and orders go to Hyperliquid testnet, on your own Hyperliquid account.
You need the mandate flow first: Offers and accept and Limits discovery. Which door this is, and the whole path in one table: The four doors.
1. Find operators#
GET /torii/doors/operators?kind=signal&limit=20&offset=0 MCP torii_doors {"operators": "signal"}
GET /torii/gallery?type=signal_subscription MCP torii_gallery_list {"type": "signal_subscription"}
GET /torii/gallery/{id} MCP torii_gallery_list {"id": 17}
All open, no key. limit is 1 to 100 (default 20). kind is signal or vault; anything else is 400 invalid_parameter.
An operator entry:
{"kind": "signal", "name": "alpha", "ntrt_id": 12, "tags": [], "vault_id": 42,
"recorded": {"vault_id": 42, "days_live": 50, "adherence_headline": "adherent", "signals_30d": 3,
"latest_signal_ts": "2026-10-14T09:00:00+00:00", "href": "/forge/vault/42",
"source": "nw_pilot_xray_daily, nw_forge_signals, nw_pilot_vaults"},
"cited": [],
"provenance": {"recorded": "computed by NightWatch from its own tables",
"cited": "claims the seller supplied; not verified, never ranked"}}
The list answers {kind, total, limit, offset, items, ranking}; ranking reads "sorted by recorded only: days_live desc, signals_30d desc". Values above are illustrative.
2. Read the track record#
recordedis computed by NightWatch. It is the only thing the list sorts by.cited[]is what the seller says:url,source_name,as_of_date,claim_text. It is shown apart, is not verified, and never ranks. Never add the two together or compare them.- House books carry
"tags": ["house"]andntrt_id: null.
To see the operator's signals: GET /forge/vaults/{vault_id}/signals (free, older than 24 hours, plus counts, latest_signal_at, rules_hash, mirroring_paused), or the paid live read GET /forge/vaults/{vault_id}/signals/live (MCP forge_signals with live: true). Price and rules: Operators publish signals. A paused vault (mirroring_paused: true) refuses plans.
3. Adopt: get a draft#
POST /torii/gallery/{id}/adopt MCP torii_gallery_adopt {"listing_id": 17}
{"params": {}, "agent_id": "<your bot's agent id>"}
Both fields optional. agent_id defaults to the key's own agent when you call with a key. For a strategy_template, params overrides its editable parameters inside their bounds (an unknown name or an out-of-range value is 400 invalid_parameter).
It creates nothing, signs nothing and moves no money. Answer:
{"draft": {"purpose": "Adopt 'Alpha signals' (gallery listing 17, signal_subscription) on a test venue. Feed",
"budget_usdc": "50.00", "per_tx_cap_usdc": "25.00", "daily_cap_usdc": "50.00",
"markets": ["hyperliquid"], "horizon_days": 14, "stop_floor_bps": 300, "on_floor": "keep",
"payout_rule": {"kind": "share_of_verified_gain", "bps": 0}, "agent_id": "<id>"},
"agent": {"kind": "own_bot", "agent_id": "<id>"},
"source": {"listing_id": 17, "product_type": "signal_subscription", "params": {}, "ref_vault_id": 42},
"venue": {"market": "hyperliquid", "kind": "test venue (Hyperliquid testnet)", "real_money": false, "network": "testnet", "why": "..."},
"created": false,
"next": [{"step": "Review and edit the numbers", "method": "POST", "path": "/torii/mandates"},
{"step": "Sign the mandate (the owner, in a session)", "method": "POST", "path": "/torii/mandates/{id}/sign"},
{"step": "Hire: follow this listing's signals with the signed mandate", "method": "POST", "path": "/torii/gallery/17/hire"},
{"step": "Bind your Hyperliquid account to the mandate (the hire waits for it)", "method": "GET", "path": "/torii/mandates/{id}/hl-leg/approve-call"}]}
The suggested numbers are 50 USDC budget, 25 per action, 50 per day, a 3 percent stop floor, 14 days. Edit them before you create the mandate. A draft targets Hyperliquid only while the host's Hyperliquid is on testnet; otherwise it stays on the Base Sepolia vault (venue.market: base_usdc_vaults). It never targets mainnet.
409 wrong_status: a vault_deposit ("locked") or a listing that is not free ("not offered").
4. Create and sign the mandate#
The owner (a signed-in person) posts the draft to POST /torii/mandates, then signs and funds it with the existing flow: GET /torii/mandates/{id}/terms, POST .../sign, GET .../create-call, POST .../vault, GET .../fund. Your bot accepts: POST /torii/mandates/{id}/accept (MCP mandate_accept). The mandate must be signed or working for everything below. Money stays in the vault: Offers and accept.
5. Bind the Hyperliquid account (the owner)#
The owner signs one approveAgent on their own Hyperliquid account, naming Torii's trade-only key for this mandate. An agent key cannot do this step.
GET /torii/mandates/{id}/hl-leg/approve-callreturns the typed data to sign andnext.- The owner signs it and posts
{action, signature}to Hyperliquid (post_toin the answer). POST /torii/mandates/{id}/hl-leg/bind. It checks that Hyperliquid lists the key, that the account is a plain user account with no open position, and that the builder fee is approved. One live leg per owner address (409 tw_hl_leg_owner_busy).GET /torii/mandates/{id}/hl-legshowsleg.status: bound.
The owner funds the Hyperliquid account from their own wallet; the vault does not move money there. Torii's money-path v2 (the vault funds the leg, with a Base-only experience) is being built and is not live yet. All the leg's refusals are in Refusals. While the HL leg is off on the host, every one of these routes answers 404 tw_feature_disabled.
6. Hire#
POST /torii/gallery/{listing_id}/hire MCP torii_gallery_hire {"listing_id": 17, "mandate_id": "m_abc"}
{"mandate_id": "m_abc"}
mandate_id is the mandate's public id (starts m_), not a number. The caller is the mandate's owner (a session) or its assigned agent (a key, scope tx). The call is idempotent and moves no money.
Before the Hyperliquid account is bound, nothing is written:
{"ok": true, "status": "pending_leg", "hired": false, "listing_id": 17, "mandate_id": "m_abc", "vault_id": 42,
"executor": {"kind": "own_bot", "user_id": 11},
"note": "nothing written yet: the mandate's Hyperliquid account is not bound",
"next": [{"step": "Bind the Hyperliquid account to the mandate", "method": "GET", "path": "/torii/mandates/m_abc/hl-leg/approve-call"},
{"step": "Then hire again (same call; it is idempotent)", "method": "POST", "path": "/torii/gallery/17/hire", "body": {"mandate_id": "m_abc"}}]}
Bind, then call it again. Once bound:
{"ok": true, "status": "hired", "hired": true, "listing_id": 17, "mandate_id": "m_abc", "vault_id": 42,
"executor": {"kind": "own_bot", "user_id": 11}, "follow_link_created": null,
"mirror": {"mirror_id": 101, "account_address": "0x...", "rules_hash": "<hash>", "status": "active"},
"note": "your bot reads GET /forge/signals, plans each with mandate_id, and places through mandate_hl_order",
"created_money_movement": false}
What hire does: it records your consent to mirror the vault, at the mandate's bound Hyperliquid account and the vault's current rules, for whoever executes. It does that for your own bot (executor.kind: own_bot, follow_link_created: null) or for the house runner (house_runner, follow_link_created: true the first time). If the vault's rules change later, consent lapses and a plan is refused with rules_hash_stale; hire again, or consent again with the new hash.
Refusals: 404 not_found (no such listing, or its operator no longer qualifies; no such mandate), 403 not_owner (a stranger, or the wrong key and session pairing), 409 wrong_status (not a free signal_subscription or house strategy with a vault; vault deposit "locked"; mandate not signed or working; mandate paused; no agent assigned; this mandate already follows another vault, one vault per hired mandate), 400 invalid_parameter (hyperliquid is not on the mandate's allowed markets; adopt again).
7. For each new signal: plan inside the mandate#
POST /forge/signals/{signal_id}/plan MCP forge_signal_plan {"signal_id": 9031, "mandate_id": "m_abc"}
{"mandate_id": "m_abc"}
mandate_id may also be a query parameter. Your key must be the mandate's agent (or the owner's session). Inside a mandate the plan is sized from the mandate: its worth when money is committed and the ledger is clean, otherwise its budget. It never reads or uses your Hyperliquid account value. It places nothing.
A plan for a live signal costs what a live read costs and is free if you already paid to read that signal; a plan for a signal over 24 hours old is free. charged says whether this call paid.
{"order": {"coin": "xyz:SKHX", "side": "buy", "qty": 150.0, "limit_hint": 10.0, "builder": null, "est_fee_usd": 0.6},
"checks": [{"name": "mandate_caps", "ok": true, "detail": "order within the mandate's per-transaction and daily caps"}],
"refused": null, "charged": false, "historical": false,
"signal": {"id": 9031, "ts": "2026-10-15T09:12:04+00:00", "is_live": true},
"mandate": {"mandate_id": "m_abc", "size_base": 2000.0, "size_base_source": "worth",
"caps": {"per_tx": 100000.0, "daily": 100000.0, "approval_above": null},
"spent_today": 0.0, "needs_owner_approval": false, "verdict": "ok", "rule": null,
"execution_note": "..."},
"needs_owner_approval": false,
"grade": null, "provisional": null, "capped_notional_usd": null, "impact_bps": null, "session": null,
"disposition": null, "retry_at": null}
Values illustrative; fields from the live-signal liquidity checks are omitted when empty.
Read mandate.verdict:
verdict | Meaning | What to do |
|---|---|---|
ok | Within the mandate's caps | Place it |
needs_owner_approval | Above the owner's approval amount; refused is null | Do not place it. Honor the verdict in your bot: do not call hl-order for this signal. The house runner does the same (see House strategy) |
refused | refused holds the reason, mandate.rule names the mandate rule | Do not place. over_per_tx_cap and over_daily_cap are the mandate's; check rows name any other reason (universe, liquidity, leverage, paused vault) |
Refusals before a plan exists: 404 not_found (signal, vault or mandate), 403 not_owner (you are neither the mandate's owner session nor its agent), 409 wrong_status (the mandate is not signed or working, or is paused). These never charge. A plan can also refuse for the vault's own rules: mirroring_paused, a market outside the vault's universe, liquidity grade, a superseded signal. historical: true means the signal is no longer live; the plan is computed but is not a current instruction.
The plan's mandate.execution_note is read from the host's switches at request time. With TW_HL_LEG_ENABLED on and hyperliquid among the mandate's markets it says orders go through the mandate's Hyperliquid leg (testnet or mainnet, per TW_HL_NETWORK) on the owner's own account via POST /torii/mandates/{id}/hl-order. Otherwise it says the leg is off (or hyperliquid is not among the mandate's markets) and only the Base Sepolia test target is reachable. Check GET /torii/mandates/{id}/hl-leg (leg.status: bound) before you place.
8. Place the order through the mandate#
POST /torii/mandates/{id}/hl-order MCP mandate_hl_order
{"coin": "xyz:SKHX", "side": "buy", "usd": 1500.0, "why": "signal 9031"}
Send exactly one of usd (opening notional; Torii derives the size from the live mark) or size (coins). For an open, use the plan's qty times limit_hint as usd. Torii checks the mandate (leg bound, mandate working and not paused or stopped, caps, open notional, builder fee, rate limit), signs with the mandate's key and sends one marketable IOC order to your Hyperliquid account. You never hold a key. Full rules: Reporting events.
{"ok": true, "status": "accepted", "seq": 1, "cloid": "0x...", "coin": "xyz:SKHX", "side": "buy", "size": "150.0",
"notional_usd": "1500.000000", "reduce_only": false,
"fill": {"avg_px": "10.01", "total_sz": "150.0", "oid": 123456}, "error": null, "event_id": 88}
status is accepted, rejected or unknown (no answer from Hyperliquid: read before you repeat). The tool is hidden from tools/list while the HL leg is off.
9. Report the fill#
POST /forge/signals/{signal_id}/fills MCP forge_signal_fill
{"account_address": "0x<the leg's owner address, checksummed>", "filled_at": "2026-10-15T09:13:02Z",
"price": 10.01, "qty": 150.0, "side": "buy", "venue_order_id": "123456"}
A plain write with no charge. side must equal the signal's side, filled_at must not be in the future, and account_address must be one of your active mirrors on the signal's vault (hire made it). A signal a later one superseded is refused 409. Answer: {"ok": true, "signal_id": 9031, "vault_id": 42, "recorded": true, "fill_id": 5, "source": "self_report"}; a repeat of the same report says recorded: false. It shows in GET /forge/vaults/{id}/mirrors/me as self_report. Also report the trade to the mandate with mandate_report_event, as before.
To stop following: DELETE /forge/vaults/{vault_id}/mirrors (MCP forge_mirror_revoke; add account_address to revoke one). Later plans for that vault are refused until you consent again. Consent by hand, without hire, is POST /forge/vaults/{vault_id}/mirrors (MCP forge_mirror_consent, scope sign).
With your own bot or the house runner#
| Your own bot | House runner | |
|---|---|---|
| Who executes | Your bot, steps 7 to 9, for each signal | The runner, every two minutes, no calls from you |
| Mandate's agent | Your bot's agent id | The house runner's agent id (adopt picks it for a house listing when you name no agent) |
executor.kind in hire | own_bot | house_runner |
| Mirror consented for | Your bot's account at your leg's address | The runner's account at your leg's address |
House strategy describes the runner.
What is not available#
- A signal quantity. Sizes come from your mandate.
- Mainnet, or real money on door 4.
- A push when a signal arrives. Poll
GET /forge/vaults/{id}/signals/liveor the older feed. - An approval link or an approval item for a plan above the approval amount, and enforcement of that amount on
hl-order. - Signed webhooks for fills.