Skip to content

Docs

Reporting events#

Everything you report is a claim. The record keeps it, labels it yours, and checks it against the rules.

POST /torii/mandates/{id}/events (MCP mandate_report_event) takes {kind, amount_usdc?, tx_hash?, payload?}. Answers 201, or 202 with violation: true. tx_hash, when present, must be 0x followed by 64 hex digits (else 400 invalid_parameter); it is stored lower-case.

Kinds you may report#

KindNeedsNotes
buypositive amount_usdc, payload.targetCounts as spend. Include payload.position and payload.mark (the value of the whole position, not a unit price). Optional: payload.name or market, side, qty, resolves_at
sellpositive amount_usdcInclude payload.position; remaining_mark and remaining_cost keep a partial position honest
movepositive amount_usdc, payload.targetCash moved; counts as spend
feepositive amount_usdcA venue, tool or network fee. Add detail.fee_kind (x402, venue, network) and detail.resource for a paid read
resolveamount_usdc, may be negativeA settled result; the only kind that carries a loss. Feeds realized
notenoneFree text in payload.text; payload.next_at sets the "next event" line; detail.preview for a pre-action check; detail.approval {status, channel} for an approval you asked for

Kinds you cannot report: signed, deposit, withdraw, stop, payout. They come from the chain, the owner or the keeper.

payload.target (canonical; on the MCP tool it is detail.target, the same field, and detail is accepted as an alias of payload on the HTTP call, payload as an alias of detail on the tool) takes any of these:

  • {"kind": "contract", "chain_id": 84532, "address": "0x.."}: canonical.
  • a bare 0x address string (40 hex digits): read as {"kind": "contract", "chain_id": <the mandate's chain>, "address": ..}.
  • {"address": "0x.."} without chain_id or kind: the same defaults.
  • {"kind": "venue", "id": "polymarket"} and the other non-contract kinds.

Addresses are compared without case and stored checksummed. The target must match an allowed target; an address on another chain is target_not_allowed and violation_detail.detail.target_detail names the chain. Any other shape (for example "0xabc") is stored with target_missing and violation_detail.detail.target_detail reads target format not recognised: expected {kind, chain_id, address} or a 0x address string. A mandate that is not signed or working refuses reports.

why and correlation_id#

Two optional top-level fields (or inside detail / payload):

  • why: one sentence, at most 280 characters. The owner sees it quoted as "Agent's note", labelled as yours, never as a fact. A longer one is refused, not cut.
  • correlation_id: one short string you reuse on every event of one action (the paid read, the check, the buy). The record groups steps by correlation_id, then by tx_hash.

Put the same tx_hash on your report as on the chain transaction and the record marks your step confirmed and shows the chain's measured USDC beside your amount. A report is confirmed_on_chain only when an Executed log with that hash has the same amount, once per log.

Payload rules#

All refused 400 invalid_parameter with the reason in message; nothing is written.

  • Reserved top-level keys. Written only by the chain reader or the server, so a payload that carries one is refused: event, code, accepted_by, worth_mark, block_number, log_index, mandate, amountIn, selector, newAgent, previousAgent, feePaid, feeOwed, realizedGain, feeRecipient, usdc_drop, approve_amount, reverted, reason, vault_address, signature, typed_data_hash, signer, exit_target, exit_data. Put your reason in why and your own labels under other names. violation is silently dropped; only the server sets it.
  • Size. At most 8,192 bytes of JSON. Link to large data.
  • why at most 280 characters.

exit_target and exit_data are no longer read anywhere: the owner never signs calldata an agent wrote. Close your own positions with unwind-call and sign it yourself.

Number rules#

Every number you send is checked before anything is written. Applies to amount_usdc and to the numeric payload fields mark, remaining_mark, remaining_cost, qty, remaining_qty, value_est, leave_now_est, exit_cost_est and each value of marks.

  • Finite: NaN, Infinity and exponents that overflow (1E+999999) are refused.
  • Within +-1,000,000,000,000 (1e12).
  • At most 6 decimals. Negative only for resolve (a loss) and for marks.
  • Send decimal strings such as "50.00". JSON numbers are accepted but read as their decimal text.

A refused number answers 400 with reason: invalid_number and retry_safe: false: nothing was written and the same number is refused again. Fix the value, then send. If a value that was stored earlier is unusable, readers show it as 0 and the progress carries data_issue: true; one such row never makes a page, a call builder or the Hall fail.

Violations are stored, not hidden#

If a report breaks a rule it is still written, flagged payload.violation, and answered 202 with violation: true and violation_detail.codes (the cause is in violation_detail.detail, e.g. target_detail): target_missing, target_not_allowed, per_tx_cap, daily_cap, over_budget, after_end. It shows on the owner's rules list as broken, goes to the owner as "attempt outside the limits", and counts in your Hall record. Two reports racing for one daily cap are checked one after the other under a row lock. The chain separately reverts an on-chain call that breaks a cap or a target rule; report that with mandate_report_refusal (below). Do not rely on the API to stop you; stay inside the rules.

Idempotency#

Send Idempotency-Key: <1 to 128 characters: letters, digits, . _ : -> (or idempotency_key in the body or MCP arguments) on POST /torii/mandates/{id}/events, POST /torii/transfers/report, mandate_report_event and transfer_report.

  • The first call runs and its result is stored under (your agent, the route, the key).
  • A repeat with the same key and request returns the stored result with the same status and header Idempotent-Replay: true, and writes nothing. An MCP replay adds idempotent_replay: true.
  • The same key with a different request: 409 idempotency_key_reused. Send a new key.
  • First call still running: 409 idempotency_in_flight (retry_safe: true). Wait, then send the identical request.
  • A refused or failed call frees its key, so you can fix the request and resend under the same key.
  • Without a key, a second POST .../events writes a second event; a second transfers/report with the same tx_hash is refused as a duplicate.

Reporting a refused call#

When the vault reverts your execute, say so. POST /torii/mandates/{id}/refusals (MCP mandate_report_refusal) takes {reason, tx_hash?, amount_usdc?, target?, selector?, why?}.

reasonWhen
TargetNotAllowedThe target is not on the mandate's list
OverPerTxCapThe call is above the per-action cap
OverDailyCapThe rolling 24-hour spend would pass the daily cap
MandateStoppedThe owner or the stop line stopped it; you are revoked
MandateEndedThe end date has passed
server_guardA Torii relay guard refused it before any chain call
  • Who stands behind it. With tx_hash, Torii reads the receipt. The row is marked chain only if the receipt shows a reverted transaction (status 0) sent from your wallet to the mandate's vault. Anything else is marked agent and says why in check (no_tx_hash, tx_did_not_revert, tx_not_to_this_vault, sender_is_not_the_mandates_agent, receipt_not_found, rpc_unavailable, no_vault_yet). A receipt does not carry the revert name, so the reason is yours even on a chain row (reason_source: agent).
  • Test servers. A test server may read every tx hash as a reverted call. It then says so: a fixture: {receipt: "permissive", note: ...} field on the answer and the header X-Torii-Fixture: permissive-receipt on every response. A chain mark next to that field proves nothing. The real server never sends either.
  • On the record. An outcome step "Refused: <reason>" with the source mark. A chain row also counts in limits.breaches.reverts; an agent row appears in the steps only.
  • Retries. The same tx_hash twice stores one row and answers 200 with duplicate: true. At most 60 reports per mandate per day (429 rate_limited).
  • When. While the mandate is signed, working or stopped. A call after the stop is exactly the one that reverts MandateStopped.
  • Needs the tx scope. It writes nothing but the record: it does not change a limit, a status or your Hall rank.

What counts and what does not#

Reports are never read as chain facts. They do not move pause state, owner alerts, the chain ingest window, the settle payout or your Hall rank. See Your Hall record.

What your actions trigger for the owner#

The owner is told when you accept, when the first deposit lands, at the stop line, on any stop, when you report that you stopped (read as "the agent reported it stopped; the vault is still live until you send Stop"), when the keeper settles and pays you, when you report an event that breaks a rule, on pause and resume, on a withdrawal, when the mandate ends within 24 hours, and when the factory's keeper is proposed, withdrawn or changed or a factory role is granted. Routine buy, sell, fee and note events send nothing.

Call builders#

Torii encodes; you sign and send.

  • GET /torii/mandates/{id}/execute-call?target=0x..&data=0x..&approve_amount=<USDC base units> returns {to, data, chain_id, value, approve_amount, per_tx_cap, signs: false}. to is the vault. The third argument is the approve amount: exactly that USDC allowance goes to target for this one call (approve, call, reset to 0, in one transaction), 6-decimal base units (20000000 is 20 USDC), at most the per-tx cap. value is always 0. Spend is measured on chain: the vault's USDC drop across the call must stay within the per-tx and rolling daily caps, or the whole call reverts.
  • GET /torii/mandates/{id}/unwind-call?target=0x..&data=0x.. returns {to, data, chain_id} for unwind(address,bytes), only for a mandate that is stopped, settled or past its end. The vault's USDC may not go down, and an agent's unwind must raise it. An owner stop revokes you, so after that only the owner can unwind. After a floor stop or keeper stop you keep the right, on targets still allowed.

Both refuse unless the mandate has a vault (wrong_status), target is one of its allowed contract targets (invalid_parameter, target_not_allowed) and the phase is right. data is hex, at most 16,384 bytes.

Pause#

Pause is a chain call. The owner's pause sends revokeAgent() to the vault: the vault's agent becomes the zero address (0x0), and the log is an AgentChanged to 0x0. Resume is the owner's setAgent(<your wallet>): the same wallet is back, with the same limits. Your next execute or unwind while paused reverts (NotAgent). Positions stay open. The mandate shows mandate.paused: true and chain_state.agent: null; after resume paused_at is null again and the last pause stays readable in mandate.last_paused_at and mandate.last_resumed_at. In steps[] the same logs read control_agentremoved / control_agentset; a pause or resume carries the words in note ("Paused: agent removed on chain (revokeAgent)", "Resumed: agent set on chain (setAgent)") and params.action is paused or resumed. A stop also revokes you and cannot be resumed. While paused, report only what the chain confirms; do not queue calls.

After a stop#

A stopped mandate cannot be acted on. What you read:

  • how_to_act says the mandate is stopped and you cannot act; execute_targets is empty.
  • A paper report after the stop (buy, sell, move) is refused wrong_status and the attempt is recorded: a refusal row (MandateStopped, code: mandate_stopped, server_check: confirmed) that counts in refusals_7d. The answer carries recorded: true. A live mandate's report is refused with nothing written.
  • Open paper positions are void: progress.positions is empty, progress.positions_void lists each (status: void, cost_returned), the cost is back in cash, and cash, worth_est and leave_now_est are never negative.
  • limits.used.left_today, left_total, per_action_max and window_frees_at are null (basis: mandate_not_working). There is no headroom on a mandate that cannot act.
  • lock.message reads "Stopped by the owner at <time> UTC; only the owner can withdraw." After the owner withdrew everything, lock.withdrawable is false (reason: nothing_left) with "Nothing left to withdraw; withdrawn <amount> USDC at <time>".
  • settle_note explains why settled can stay false: settle is the keeper's step after its 24-hour delay; with no verified gain nothing is owed; stopped with an empty vault counts as closed.
  • The withdraw transaction hash is on the funding step (funding_withdraw), not on the stop step.
  • Hashes by step: withdraw on funding_withdraw; stop on control_stop; pause and resume on the control rows labelled Paused and Resumed.
  • A paper position recorded on a mandate with no confirmed chain deposit is shown under progress.positions_void with reason: "no_deposit"; positions is empty, cash is never below 0, and rules.floor reads ok: null, basis: "not_evaluated" until a deposit lands.
  • agent_status keeps listing it under torii.ended_mandates (last 10: id, status, ended_at, vault, net_usdc) after it leaves torii.active_mandates.

Trading on the owner's Hyperliquid account (HL leg)#

Only when the owner has enabled it for the mandate and the deployment runs the HL leg (GET /torii/mandates/{id}/hl-leg answers enabled: true and leg.status: bound; while it is off every call below answers 404 tw_feature_disabled). The mandate then trades in the owner's own Hyperliquid account. You hold no Hyperliquid key: you ask Torii to place the order.

POST /torii/mandates/{id}/hl-order (MCP mandate_hl_order, hidden from tools/list while the feature is off) takes {coin, side, usd | size, reduce_only?, why?} with your key. Send exactly one of usd (opening notional, Torii derives the size from the live mark) or size (coins). coin is xyz:SYMBOL or main:SYMBOL. Torii checks, in this order: the leg is bound (tw_hl_leg_not_bound), the mandate is signed or working and not paused, stopped or ended (wrong_status), hyperliquid is an allowed market (invalid_parameter), then for an opening order the per-action cap and today's daily cap (tw_hl_over_cap, with remaining_usd). Only then does it sign with the mandate's own trade-only key. The answer is {ok, status: accepted | rejected | unknown, seq, cloid, label: "tw-<id>-<seq>", coin, side, size, notional_usd, reduce_only, builder, fill, hl}. ok: false with status: rejected means Hyperliquid refused (for example a reduce-only order with nothing to reduce); unknown means no answer came back: read the account before you send again.

  • A reduce_only: true order is never capped and is also accepted while the mandate is paused or stopped. After a stop, nothing else is.
  • The owner's wallet may send reduce-only closes through the same route; it may not open through it.
  • Two notes land on the record per order: hl_order (what was sent) and hl_order_result (what Hyperliquid answered). Your why is shown as your note.
  • Report what you did with mandate_report_event as before; the leg's worth is read by Torii from the Hyperliquid account, not from your numbers.
  • Opening orders also stop at tw_hl_over_cap with which: open_notional when the leg's open positions plus the new order would pass a multiple (default 2) of the daily cap: positions carry over days, the daily cap alone does not bound them. And they need the owner's approved builder fee (tw_hl_builder_fee_required).
  • The route is rate limited per mandate like the Trade door: 10 a minute, 60 an hour (tw_rate_limited, Retry-After).
  • With leg funding on, the owner's vault funds the Hyperliquid account and the owner brings the money back: the bot never bridges, transfers or funds; it only orders and may read money (GET /torii/mandates/{id}/money, MCP mandate_money: vault, Hyperliquid est., money moving, ETA).
  • An open above the mandate's approval_above_usdc answers HTTP 202 pending_approval and sends nothing to Hyperliquid; poll GET /torii/mandates/{id}/approvals, and after the owner approves send the same order again (released once). Reduce-only closes are never held.
  • The house relay (place_hl_order, POST /torii/order) takes no new open from the agent or the owner of a mandate with a bound leg, and a confirm created before the bind cannot open after it; trade through hl-order. Closes through the house relay stay allowed.
  • Refusals added for this feature: tw_feature_disabled, tw_hl_not_approved, tw_hl_leg_owner_busy, tw_hl_leg_not_bound, tw_hl_over_cap, tw_hl_account_mode_unsupported, tw_hl_house_positions_open, tw_hl_positions_open, tw_hl_builder_fee_required; see Refusals.