Skip to content

Docs

Limits discovery#

Read your remaining room before every action. One block, every number sourced.

GET /torii/mandates/{id} and MCP mandate_get carry one limits block next to clauses. The answer to POST .../events returns the same block after your event, so a loop needs no extra read. There is only this one limits key. A mandate read from an older build has no limits: treat that as unknown and read clauses 2 and 3.

Shape#

{
  "allowed": {"per_tx": "25.000000", "per_day": "60.000000", "stop_floor": "170.000000",
              "budget": "200.000000", "ends_at": "2026-11-10T09:00:00Z",
              "source": "terms", "caps_source": "columns"},
  "used": {
    "spent_24h":        {"value": "24.000000", "source": "chain", "basis": "chain_window"},
    "left_today":       {"value": "36.000000", "source": "chain", "basis": "chain_window"},
    "left_total":       {"value": "176.000000", "source": "events", "basis": "chain_events"},
    "per_action_max":   {"value": "25.000000", "source": "chain", "basis": "chain_window"},
    "window_frees_at":  {"value": "2026-10-13T08:10:00Z", "source": "events", "basis": "rolling_24h"},
    "largest_action_24h": {"value": "24.000000", "source": "events", "basis": "chain_events"},
    "spent_total":      {"value": "24.000000", "source": "events", "basis": "chain_events"}
  },
  "breaches": {"count": 0, "last": [], "reverts": {"count": null, "source": "none"}, "outside_limits": "none"}
}

The values above are illustrative.

Fields#

FieldMeaning
allowedper_tx, per_day, stop_floor, budget (6-decimal strings), ends_at, source: terms, caps_source (columns: caps the owner set as fields; legacy_caps_entry: an older row; budget: caps left out default to the whole budget)
used.spent_24hSpend in the last 24 hours. From the vault itself when the chain is readable, else from chain-confirmed events, else from your own reports
used.left_todayper_day minus spent_24h, never below zero
used.left_totalbudget minus all spend, never below zero
used.per_action_maxThe largest single action that stays inside every limit right now: the smallest of per_tx, left_today and left_total
used.window_frees_atWhen the oldest spend in the 24-hour window leaves it, so room opens again. null (basis: nothing_in_window) when nothing is in the window. The window is rolling, not a calendar day
used.largest_action_24h, used.spent_totalFacts from the same ledger
breachescount, the last few rule breaks (at, kind, amount_usdc, codes, clauses, source), reverts, and outside_limits: none, flagged or unknown

Usage and refusals on the same read#

GET /torii/mandates/{id} (and mandate_get) also carries two small blocks for live widgets, derived from the same ledger as limits. The detail read is for page load (it refreshes from the chain and loads the whole ledger); polling widgets read GET /torii/mandates/{id}/live instead, which gives the same keys from stored rows only.

  • usage: {today_usdc, largest_action_usdc, window: "rolling_24h", source}. today_usdc is used.spent_24h and largest_action_usdc is used.largest_action_24h, so on the detail read the widget never disagrees with the cap. On /live the figure is always the events figure (source: "events"); the detail read may show the chain's own spentInWindow instead (usage.source: "chain"), so the two can differ slightly until the vault's events are ingested. Both are null (never "0.000000") while nothing is known.
  • refusals_7d: the number of refused attempts on the record in the last 7 days: agent refusal reports, chain-proved reverts and the keeper's owner_declined notes, each counted once (a chain row that only proves an agent's report is not a second one). It counts the whole ledger, not just the 150 steps the record shows. null on a draft.

The owner's read adds agent: {agent_id, name, last_seen_at, channel}: when the agent's key was last seen (the first call in each minute) and over which door (mcp or http). Both are null until the first call is recorded. You do not see this block on your own read.

Sources and bases#

Every number in used is {value, source, basis}.

  • value is a 6-decimal string, or null when unknown. Never "0.000000" for unknown.
  • source says who knows it: chain, events, terms or none.
  • basis says how it was derived: chain_window, chain_events, chain_events+agent_reports (chain rows plus the agent reports no chain log claimed, such as x402 and venue spend; the same number the cap enforces), agent_reports, none_recorded, rolling_24h, nothing_in_window.

When chain Executed events exist, your own buy and move reports are not added on top (single counting).

Rules of thumb#

  • Plan each action to stay under both used.left_today and allowed.per_tx. The contract refuses anything above either.
  • The chain counts the rolling 24 hours in hourly steps, so stay a little under left_today.
  • fees.net_after_fees_est and fees.net_after_fees_realized are separate figures. Never add them.
  • outside_limits is none only when the mandate is signed and nothing is flagged.
  • A refused or flagged attempt counts in breaches where the owner sees it.

Clauses 6 and 7: what Torii holds and what it closes#

Read clauses[5] (asking) and clauses[6] (at the stop line). Each has status (enforced or stored_pending), enforced_by and a scope object.

{"n": 6, "key": "asking", "status": "enforced", "enforced_by": "Torii guard",
 "text_en": "asks you before any action above 25.00 USDC; enforced by Torii guard for relayed actions; direct vault calls are only limited by the contract caps",
 "scope": {"covers": "relayed_actions", "relayed": ["execute-call", "mandate_execute_call"],
           "not_covered": "direct vault calls signed and sent by the agent",
           "text_en": "enforced by Torii guard for relayed actions; direct vault calls are only limited by the contract caps"}}
{"n": 7, "key": "at_stop", "status": "enforced", "enforced_by": "Torii guard",
 "scope": {"covers": "relayed_positions", "markets_with_close": ["hyperliquid"], "markets_without_close": [],
           "keeper_dry_run": false, "text_en": "closed by the Torii keeper for positions opened through Torii's relay; ..."}}

scope is null when the clause asks nothing (clause 6 mode: own, clause 7 mode: keep). Clause 7 is stored_pending when any market has no close, or while the keeper runs in dry run; markets_without_close names them.

Clause 6 in your loop. If the owner set an approval amount and your execute-call / mandate_execute_call is above it, you get HTTP 202 {status: "pending_approval", approval: {id, status: "pending", amount_usdc, threshold_usdc, expires_at}} and no calldata. Poll GET /torii/mandates/{id}/approvals or MCP mandate_approvals (approval_id, status filters). When the item reads approved, ask for the same call again (same target, data and amount): it is released once and the item becomes executed. A declined item makes the next ask fail with owner_declined (HTTP 409) and the record shows Refused: owner_declined. An item not decided in 24 hours reads expired; asking again opens a new one. At most 10 items wait at once. A call you sign and send without asking Torii for calldata is not held; only the contract caps limit it.

Clause 7 in your record. When the chain shows worth below the stop line (or the vault stopped) and the owner chose close, the keeper records one stop_line step per market (closed, no_position, failed, close_unavailable), each marked Computed by Torii. Markets without a close are yours to unwind with unwind-call.