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.
Kinds you may report#
| Kind | Needs | Notes |
|---|---|---|
buy | positive amount_usdc, payload.target | Counts 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 |
sell | positive amount_usdc | Include payload.position; remaining_mark and remaining_cost keep a partial position honest |
move | positive amount_usdc, payload.target | Cash moved; counts as spend |
fee | positive amount_usdc | A venue, tool or network fee. Add detail.fee_kind (x402, venue, network) and detail.resource for a paid read |
resolve | amount_usdc, may be negative | A settled result; the only kind that carries a loss. Feeds realized |
note | none | Free 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
0xaddress string (40 hex digits): read as{"kind": "contract", "chain_id": <the mandate's chain>, "address": ..}. {"address": "0x.."}withoutchain_idorkind: 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 bycorrelation_id, then bytx_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 inwhyand your own labels under other names.violationis silently dropped; only the server sets it. - Size. At most 8,192 bytes of JSON. Link to large data.
whyat 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,Infinityand 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 addsidempotent_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 .../eventswrites a second event; a secondtransfers/reportwith the sametx_hashis 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?}.
reason | When |
|---|---|
TargetNotAllowed | The target is not on the mandate's list |
OverPerTxCap | The call is above the per-action cap |
OverDailyCap | The rolling 24-hour spend would pass the daily cap |
MandateStopped | The owner or the stop line stopped it; you are revoked |
MandateEnded | The end date has passed |
server_guard | A Torii relay guard refused it before any chain call |
- Who stands behind it. With
tx_hash, Torii reads the receipt. The row is markedchainonly if the receipt shows a reverted transaction (status 0) sent from your wallet to the mandate's vault. Anything else is markedagentand says why incheck(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 thereasonis yours even on achainrow (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 headerX-Torii-Fixture: permissive-receipton every response. Achainmark 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
chainrow also counts inlimits.breaches.reverts; anagentrow appears in the steps only. - Retries. The same
tx_hashtwice stores one row and answers200withduplicate: 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
txscope. 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, and when the mandate ends within 24 hours. 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}.tois the vault. The third argument is the approve amount: exactly that USDC allowance goes totargetfor this one call (approve, call, reset to 0, in one transaction), 6-decimal base units (20000000is 20 USDC), at most the per-tx cap.valueis 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}forunwind(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#
When the owner pauses, the contract calls revokeAgent(). Your next execute or unwind reverts (NotAgent). Positions stay open. The mandate shows mandate.paused: true and chain_state.agent: null. On resume, setAgent puts the same wallet back with the same limits. A stop also revokes you and cannot be resumed. While paused, report only what the chain confirms; do not queue calls.