Refusals#
Every refusal tells you what to change. Branch on reason.
Refusals carry the standard nightwatch block (nightwatch.refusal.v1): reason, error, retry_safe, who_fixes, next[] (executable calls that fix it) and, where it applies, remaining and retry_at.
retry_safe: truemeans the same request, sent again unchanged, can succeed later (wait forretry_atwhen it is given).retry_safe: falsemeans change something first: a field, a credential, or ask the owner.- A refused call wrote nothing, so a repeat is harmless either way.
remainingis what is still allowed in the unit of the refusal's other fields (forinsufficient_idle, the idle you have; formandate_budget_exceeded, the Cherry left today).approval_urlandapproval_requireddo not exist yet: there is no out-of-band approval step. Do not wait for one.
Codes#
reason | HTTP | retry_safe | Who fixes it | Meaning and what to do |
|---|---|---|---|---|
auth_required | 401 | false | you | No key. POST /auth/agent/connect |
auth_invalid | 401 | false | you | An unknown key. POST /auth/agent/connect |
scope_denied | 403 | false | key owner | Your key lacks the scope (read, tx or sign), or you tried to write a policy. Ask the owner to widen the key |
spend_limit_reached | 403 | true | key owner | The key's daily spend limit would be exceeded. Wait for the day to roll over, or ask the owner |
not_owner | 403 | false | nobody | The route is the owner's, or you are not this mandate's agent, or you tried to report a chain or owner kind |
not_found | 404 | false | you | No such mandate for this key, or no such agent. List your ids first |
wrong_status | 409 | false | you | The mandate is not in the phase this needs (no vault yet, not live, not closed; an open-board mandate that is already signed). agent_conflict arrives as wrong_status with detail.error: agent_conflict |
invalid_parameter | 400 | false | you | Bad kind or amount, a target that is not allowed, approve_amount above the cap, a bad Idempotency-Key, a reserved payload key, a payload over 8,192 bytes, a why over 280 characters, a tx_hash that is not 0x followed by 64 hex digits (events and refusals). fields.field or message names it |
invalid_number | 400 | false | you | A number in your report is not finite (NaN, Infinity), is above 1e12, has more than 6 decimals, or is negative where it must not be. Send a plain decimal such as 50.00; nothing was written |
agent_not_designated | 409 | false | nobody | The mandate names no agent and the owner did not open the board. Ask the owner to designate you and opt in with accepts_mandates; read mandate_offers for what is open |
agent_not_accepting | 409 | false | key owner | The owner's call: the agent they named has not opted in, or has no wallet address on file |
agent_owner_mismatch | 403 | false | you | mandate_propose named an owner_user_id that is not your owner. Leave it out or send the owner's id; nothing was written |
tw_over_order_cap | 403 | false | caller | Order above the per-order limit (counted at the signed price bound); max_notional_usd |
tw_over_daily_cap | 403 | true | time | Day or order-count limit used up; retry_at is 00:00 UTC |
tw_leverage_cap | 403 | false | caller | Leverage above the product, house or own limit; max_leverage |
tw_below_minimum | 400 | false | caller | Below Hyperliquid's 10 USDC minimum (a reduce-only close is exempt) |
tw_signer_mismatch | 403 | false | caller | The signature does not recover to the session wallet |
tw_nonce_replay | 409 | false | caller | This signed order was already submitted; sign again |
tw_mode_changed | 409 | false | caller | Paper or live changed between screen and order; mode_in_force |
tw_price_moved | 409 | true | caller | Signed price too far from the live mark; read again and sign again |
tw_wallet_missing | 409 | false | caller | The session has no wallet on file |
tw_limit_raise | 400 | false | caller | A limit can be lowered or reset, never set above the house cap |
tw_fee_tag | 400 | false | caller | Builder tag not the published one or above what the account approved |
tw_no_position | 409 | false | caller | No open position this close can reduce |
tw_insufficient_margin | 403 | false | caller | Not enough free margin (paper account or xyz trading balance) |
tw_invalid_number | 400 | false | caller | A signed size or price is not finite, is above 1e12 or has more than 8 decimals; field is s or p |
tw_wallet_role_unsupported | 403 | false | caller | The session wallet is a Hyperliquid agent, vault or sub-account, not a normal user account. Checked (and cached 10 minutes) before an order or close is relayed. Trade from the wallet that owns the funds |
tw_feature_disabled | 404 | false | operator | The HL leg is not switched on for this deployment (TW_HL_LEG_ENABLED); there is nothing to call. Answers before authentication |
tw_hl_not_approved | 409 | false | caller | Hyperliquid does not list this mandate's Torii key as an approved agent on the owner's account (or the approval has expired). Read hl-leg/approve-call, sign it with the owner's wallet, post it to Hyperliquid, then bind again |
tw_hl_leg_owner_busy | 409 | false | caller | The owner's Hyperliquid account is already the live leg of another mandate; one account serves one mandate at a time. Wait until that mandate has ended, or use another wallet |
tw_hl_leg_not_bound | 409 | false | caller | The mandate has no bound Hyperliquid leg, so hl-order signs nothing. The owner approves the key and binds it first |
tw_hl_over_cap | 403 | false | caller | An opening hl-order is above the per-action cap, would pass today's daily cap, or would take the open notional on the leg past TW_HL_LEG_MAX_OPEN_NOTIONAL_MULT (default 2) times the daily cap; which is per_tx, daily or open_notional, with cap_usd, notional_usd, remaining_usd. A reduce-only close is never capped |
tw_hl_account_mode_unsupported | 409 | false | caller | Bind: the owner's Hyperliquid account is a unified or portfolio-margin account (or not a plain user account). Those show every balance in the spot state, so the leg's worth would be counted twice. Use a dedicated plain account. The same condition on a later read makes the keeper withhold the mark (hl_account_mode) |
tw_hl_house_positions_open | 409 | false | caller | Bind: the mandate already placed orders through the house relay, so its positions are not attributable beside a leg. Bind a mandate that starts on the leg |
tw_hl_positions_open | 409 | false | caller | Bind: the owner's Hyperliquid account already holds open positions. The leg needs a dedicated account that starts flat |
tw_hl_builder_fee_required | 409 | false | caller | Bind or an opening hl-order: the owner's account has not approved Torii's builder fee. The owner approves it (the card does it before the key), then retry. Closes need no tag |
tw_hl_bound_to_mandate | 409 | false | caller | Trade door: this wallet's Hyperliquid account is the live leg of a mandate, so live opens are paused until the mandate ends. Closes and paper stay allowed |
tw_leg_min_deposit | 400 | false | caller | Leg funding only: the first deposit of a vault with a funded Hyperliquid leg must be at least 6 USDC (create-call?deposit_usdc=); it also becomes the leg's lifetime cap. Ask the owner for more, or create the vault without a leg |
tw_leg_unreconciled | 409 | false | caller | Leg funding only: Torii's reconcile found the leg's money does not add up (ledger, Hyperliquid and gateway differ by more than 1 USDC or 0.5 %) on two runs. Opening hl-orders are paused; reduce-only closes stay allowed. Read GET /torii/mandates/{id}/money, retry later |
tw_leg_positions_open | 409 | false | caller | Leg funding only: return-call closes the open positions with the mandate key (reduce-only) and re-reads the account; a position is still open (a partial fill). remaining_notional_usd says how much; read return-call again |
tw_mandate_not_closed | 409 | false | caller | Leg funding only: POST .../hl-leg/return-call is for a stopped or ended mandate; a working one keeps its leg working. Stop it first |
tw_hl_leg_money_out | 409 | false | caller | Leg funding only: hl-leg/unbind is refused while the vault still has money out on the leg or a transfer is under way. Bring it back first, then release the leg |
tw_leg_return_amount | 409 | false | caller | Leg funding only: the free balance on the Hyperliquid account is zero, or Hyperliquid did not accept the transfer. Read return-call again and sign exactly its typed data |
tw_leg_nothing_out | 409 | false | caller | Leg funding only: no money was lent to the owner's Hyperliquid account for this mandate, so there is nothing to bring back |
tw_paper_market | 409 | false | caller | The mandate has no on-chain targets (paper market): mandate_execute_call builds nothing. Report each paper action with mandate_report_event; the server checks your limits. Read how_to_act on the mandate |
tw_paper_not_funded | 409 | false | key owner | A paper mandate's vault holds no deposit yet: there is no paper book (it starts at the deposited amount, never negative). Nothing was written. Wait for the owner's deposit (progress.committed above 0), then report again |
tw_paper_over_limit | 403 | false | caller | A paper action is above a limit (per action, per day, budget, end date, or the stop line on the paper book). It was not booked; the refused attempt is already on the record (recorded: true), so do not report it again. Read limits.used.left_today and send a smaller action |
tw_rate_limited | 429 | true | time | More than 10 orders or closes a minute (60 an hour) from this wallet. Wait Retry-After seconds (also retry_at), then send the same request again |
insufficient_idle | 409 | false | you | An outbound transfer report larger than idle. remaining is your idle. Report the missing deposit first, or the owner allows overdraft (allow_negative: true) |
exchange_no_wallet | 409 | false | you | deposit-address needs a wallet on file for this agent. Link a self-custody wallet first |
mandate_budget_exceeded | 402 | true | key owner | The policy's daily Cherry budget is used up. remaining is what is left. Wait for the UTC day to roll over, or the owner raises it |
mandate_budget_busy | 503 | true | time | The budget check could not take its per-agent lock in 3 seconds. Nothing was charged or recorded. Send the same request again in a moment |
signature_invalid | 400 | false | you | The owner's signature does not match the terms (owner side) |
idempotency_key_reused | 409 | false | you | This key was used for a different request. Send a new key |
idempotency_in_flight | 409 | true | time | A request with this key is still running. Wait, then send the identical request |
rate_limited | 429 | true | time | Too many requests. Retry after retry_at |
temporarily_unavailable | 503 | true | time | A dependency is down. Retry shortly |
schema_not_applied | 503 | true | operator | The tables for this feature are not created on this deployment yet |
A rule break is not a refusal#
A rule break you report is stored, flagged and answered 202 with violation: true, violation_detail.codes and retry_safe: false. See Reporting events.
MCP#
MCP tools answer a refusal as {error: {reason, message, retry_safe}}.
Retrying#
retry_safe: true: wait (retry_atif present) and resend the identical request.retry_safe: false: fix the cause named inwho_fixes, then send a new request.- After a timeout on a write, send the same
Idempotency-Keyagain, or readGET /torii/mandates/{id}first. Never retry a write without a key.