Torii Wallet data shapes#
The JSON an agent or a client reads. Money is a 6-decimal string. Unknown is null, never "0.000000". English field names; the screens translate by code and params.
Clause#
Every mandate read carries clauses[] in this order: budget, per_action, per_day, stop_line, markets, asking, at_stop, period, agent_fee, your_floors.
{"n": 2, "key": "per_action", "text_en": "up to 25.00 USDC per action",
"params": {"amount": "25.000000"}, "enforced_by": "Contract",
"source": "perTxCap", "signed_fields": ["perTxCap"], "status": "enforced"}
| Field | Meaning |
|---|---|
n | Clause number. Receipts cite it |
key | Stable name |
text_en | The sentence the owner saw |
params | Machine values (amount, pct, days, ends_at, items) for translation |
enforced_by | Contract, Torii guard or Venue |
status | enforced or stored_pending |
Also card_header (identity line) and card_footer (standing sentences). terms_status {signed, version, digest_matches, signer_matches} is returned for non-drafts.
Limits#
See Limits discovery. Shape: {allowed, used, breaches}; each used number is {value, source, basis}.
Steps (the record)#
steps[], newest group first. A group is in reading order.
{"id": "...", "group": "corr-17", "kind": "action", "code": "buy", "status": "done",
"at": "2026-10-12T08:10:00Z", "reported_by": "agent", "confirmed_on_chain": true, "est": false,
"amount_usdc": "16.400000", "tx_hash": "0x...", "why": "Odds look low.", "note": null,
"resource": null, "target": {"market": "polymarket", "address": null, "selector": null},
"allowed_by": [2, 3, 5], "breached": [], "clauses": [2, 3, 5], "params": {}}
kind | Meaning |
|---|---|
instruction | The owner's purpose note, with clauses 1 to 10 (last in a group) |
research | A paid read (fee event with a resource) |
verify | A pre-action check (note with payload.preview) |
consent | An approval (note with payload.approval); pending steps are pinned on top |
action | An agent buy, sell or move, or a chain Executed with no agent report |
outcome | The chain's measured result next to an agent report, a resolve, a payout or a mark |
funding, control, note | Deposits and withdrawals; pause, stop; free text |
reported_by is owner, agent, chain or keeper. why is only taken from the agent's own events.
Fees#
{"tools": "0.96", "venue": "0.00", "network": "0.00",
"unclassified": {"amount": "0.00", "count": 0, "source": "none"},
"agent_fee": {"reserved_est": "0.38", "paid": "0.00", "paid_source": "none"},
"realized_total": "0.96",
"net_after_fees_est": {"value": "3.46", "est": true, "source": "events", "pct": "1.73", "formula": "worth_est - committed - agent_fee_reserved"},
"net_after_fees_realized": {"value": "0.12", "est": false, "formula": "resolve - fees - payouts"}}
The two net figures are never summed. A fee event is classed by payload.fee_kind (x402/tools/paid_read, venue, gas/network); without one it is unclassified, never folded into venue.
Progress#
worth_est, realized, floor, cash, positions_est, fees, agent_fee_reserved, leave_now_est, leave_now_basis (cash + positions_est - exit_cost_est, with spread_bps), exit_cost_est, rules[{text, ok}], next_event, status, day, horizon_days. Values labelled est are estimates. A keeper's chain mark replaces the agent's mark (worth_source: chain_mark).
Lock and chain state#
lock {withdrawable, reason, until, message} mirrors withdrawAll. chain_state {stopped, settled, agent, agent_known}. mandate.paused and paused_at for a pause.
Hall row#
{"agent_id": "4127", "name": "Halyard", "days_live": 41, "trades_total": 63, "ranked": true,
"accepts_mandates": true, "address_ready": true, "as_of": "2026-10-12T00:00:00Z",
"windows": {"7d": {"net_usdc": "...", "twr_bps": 12, "max_drawdown_bps": -40, "trades": 5},
"30d": {}, "90d": {}, "since": {}},
"delegators": {"total": 3, "working": 2, "net_7d_usdc": "...", "net_30d_usdc": "..."},
"mandates_total": 5, "floor_stops": 0, "self_stops": 0, "violations": 0,
"operator_stake_usdc": null, "operator_stake_reason": "no stake mechanism yet",
"self_reported": {"trades_total": 70, "net_30d_usdc": "...", "net_since_usdc": "...", "fees_usdc": "...", "note": "..."}}
The numbers above are illustrative. An unranked agent carries ranked: false, unranked_reason and rank_requirements.
Statement#
GET /torii/mandates/{id}/statement?week=2026-W41:
| Field | Meaning |
|---|---|
period | The ISO week in UTC |
worth {start, end} | Worth at both ends |
flows | Deposits and withdrawals |
result {realized, estimated} | estimated = worth change minus net deposits minus realized change |
fees {by_type} | Fees by type |
allowed_vs_used | The largest move and busiest day against the caps |
breaches, steps | Attempts outside the limits; the steps taken |
disclosures | Codes net_of_fees, past_results, estimates, off_switch |
GET /torii/me/statement returns {totals, mandates[], disclosures[]}.
Mandate terms (EIP-712 v2)#
MandateTerms: publicId, owner, agent, budgetUsdc, perTxCap, dailyCap, stopFloor (absolute), stopFloorBps, horizonDays, endsAt, agentFeeBps, approvalAbove (0 = acts on its own), onFloor (0 keep, 1 close), allowedTargetsHash, guardTargetsHash, issuedAt, nonce. Domain version "2". Version 1 (no caps) stays verifiable for rows signed before.
Refusal block#
nightwatch: reason, error, retry_safe, who_fixes, next[], optional remaining, retry_at. See Refusals.
Database tables#
The nw_tw_* tables, moved here from the NightWatch schema file. The schema smoke test (tests/torii_wallet/test_schema_smoke_pg.py) creates them from the owning modules and reads them back.
Mandate tables (2026-10-11, spec docs/torii-wallet/pm/TORII_WALLET_SPEC_V1.md section 2.1; code svc/common/torii_mandates.py, routes svc/api/routes/torii_mandates.py)#
- Operator apply:
python -m svc.common.schema_ensure --apply torii_mandates.agent_id= the agent'snw_users.idas text (same number the agent wallet tables hold as BIGINT).POST /torii/mandates/{id}/acceptsets it when NULL or equal. - Created by
svc.common.torii_mandates.ensure_tables(cur)(probe-first viasvc/common/schema_ensure.py; complete schema sends no DDL; no ALTER on any request path). Not yet listed in theschema_ensureoperator CLI. - nw_tw_mandates: one row per mandate.
idBIGSERIAL PK;public_idTEXT UNIQUE (m_+ 10 base32 chars, the only id the API shows);owner_user_idBIGINT (nw_users.id, never returned by the API);agent_idTEXT NULL (the agent'snw_users.idas text; the agent role is theX-NW-User-Keywhose user id equals it; set by the owner at create or byPOST /torii/mandates/{id}/assignon a draft, never first-come);open_boardBOOLEAN NOT NULL DEFAULT false (added 2026-10-11; the owner's explicit opt-in: only when true may any agent accept a mandate whoseagent_idis NULL; designating an agent sets it back to false);purposeTEXT;budget_usdcNUMERIC(18,6);horizon_daysINT;stop_floor_bpsINT (floor = budget x (1 - bps/1e4));target_bpsINT NULL;payout_ruleJSONB (default{"kind":"share_of_verified_gain","bps":1000});allowed_targetsJSONB list (default contract<mandate vault>+{"kind":"x402"}; a legacy{"kind":"caps",...}entry is read only when the new columns are NULL);per_tx_cap_usdcNUMERIC(18,6) NULL,daily_cap_usdcNUMERIC(18,6) NULL,approval_above_usdcNUMERIC(18,6) NULL (NULL = acts on its own inside the limits; stored, enforcement pending),on_floorTEXT NOT NULL DEFAULT 'keep' ('keep' | 'close'; stored, duty pending) (all four added 2026-10-11, lane 1 guardrails editor;per_tx/dailyNULL = legacy row, the factory call falls back to the caps entry then the whole budget);chain_idINT default 84532;vault_addressTEXT NULL;statusTEXT CHECK in draft, signed, working, stopped, settled, cancelled;created_at,signed_at,ends_atTIMESTAMPTZ. Indexes: (owner_user_id,idDESC), (agent_id).last_chain_refresh_atTIMESTAMPTZ NULL (added 2026-10-12 by the schema steplast_chain_refresh_at; set bysvc/worker/nw_tw_vault_poll.pyeach time it reads this mandate's vault, even when the RPC failed; the poller refreshes NULL first, then the oldest; not written in dry-run).proposed_by_agent_idTEXT NULL (added 2026-10-12; set only when the owner's own agent proposed the draft through MCPmandate_propose, never changed; the Gate lists draft rows where it is not NULL for the owner). - nw_tw_mandate_events: append-only ledger, nothing updates or deletes.
idBIGSERIAL PK;mandate_idBIGINT FK;kindTEXT (signed, deposit, move, buy, sell, fee, resolve, stop, withdraw, payout, note);amount_usdcNUMERIC(18,6) NULL (positive magnitude;resolvemay be negative);tx_hashTEXT NULL;chain_idINT NULL;payloadJSONB (position,mark,marks,target,next_at,text; a rule break addsviolation: {codes:[...], detail:{...}}, written only by the server);reported_byTEXT (owner,agent,chain,keeper);created_at. Index (mandate_id,id). Partial unique indexuq_nw_tw_events_chain_logon (mandate_id,LOWER(tx_hash),payload->>'log_index') WHEREreported_by='chain'and both are set (audit B2 N2: one vault log is one row; the chain reader inserts withON CONFLICT DO NOTHING; server-written chain notes carry nolog_indexand are outside it). Before applying it on a database that already ran the poller, check for doubles:SELECT mandate_id, lower(tx_hash), payload->>'log_index', count(*) FROM nw_tw_mandate_events WHERE reported_by='chain' AND tx_hash IS NOT NULL AND payload->>'log_index' IS NOT NULL GROUP BY 1,2,3 HAVING count(*) > 1. Numbers: an agent-sent number is finite, within +-1e12 and at most 6 decimals (refused withinvalid_numberotherwise); a stored value that is not usable reads as 0 and the progress carriesdata_issue: true. - nw_tw_chain_cursor (2026-10-12,
torii_mandatesschema owner, not required by the API): wheresvc/worker/torii_vault_events.pyresumed last.nameTEXT PK (torii_vault_events);last_blockBIGINT NOT NULL (the last block whose rows are committed; only ever moves forward);updated_atTIMESTAMPTZ. Created bypython -m svc.common.schema_ensure torii_mandatesor by the worker's ownensure_tables; nothing on a request path touches it. - nw_tw_mandate_events refused attempts (2026-10-12): a refused call is a
noterow whosepayload.refusalis{reason, source, reason_source: "agent", check, attempted?}.reasonis one of TargetNotAllowed, OverPerTxCap, OverDailyCap, MandateStopped, MandateEnded, server_guard.reported_byischainonly when the receipt proves a reverted call from the agent's wallet to the vault, elseagent. The payload carries noblock_numberorlog_indexon purpose, so such a row never moves the chain reader's resume point. At most 60 per mandate per day; one row pertx_hash. - Facts, not conclusions: no profit column.
worth_est,realized,floor,leave_now_estare derived on read bycompute_progress(realized= sum resolve - sum fee - sum payout;worth_est= cash + latest mark per open position, an estimate). An agent may report only move, buy, sell, fee, resolve, note; deposit, withdraw, signed, stop and payout come from the chain, the owner or the keeper. A violating agent event is stored withpayload.violationand flips the matchingrules[].okto false; it is never dropped.
Agent wallet tables (builder tier, spec docs/torii-wallet/pm/TORII_WALLET_SPEC_V1.md 2.3)#
- Owner:
svc/common/torii_agent_wallet.py(look-firstensure_tables, no hot-path ALTER; operator apply:python -m svc.common.schema_ensure --apply torii_agent_wallet, also stillpython -m svc.common.torii_agent_wallet). agent_id convention (glue lane): everyagent_idcolumn and API field in both Torii Wallet sections is the agent'snw_users.id(the user id itsX-NW-User-Keyresolves to), a BIGINT here and a string in JSON; it is NOTnw_agents.id. The owner is found vianw_agents.owner_user_idjoined onnw_agents.user_id = agent_id. These tables have never run on a real DB, so the schema was created correct from the start; no ALTER. Routessvc/api/routes/torii_agent_wallet.py, watchersvc/worker/nw_tw_deposit_watch.py. No exchange key or secret is ever stored; bodies carryingapi_key/api_secret-style fields are refused (400). - nw_tw_agent_policies: one row per agent.
agent_idBIGINT PK (=nw_users.idof the agent account),owner_user_idBIGINT,assets/venues/networks/stop_conditionsJSONB (stored verbatim),per_tx_cap_usdc/total_cap_usdcNUMERIC(38,6),hold_ratioJSONB,min_net_bps/max_cost_bps/slippage_bpsINTEGER,cherry_budget_dailyBIGINT,auto_vs_approvalJSONB,accepts_mandatesBOOLEAN NOT NULL DEFAULT false (added 2026-10-11; true = the agent can be designated in a mandate and is listed byGET /torii/hall?accepting=1; the PUT replaces the whole policy, so a body that omits it sets it to false),updated_at. PUT by the owner only. - nw_tw_agent_policy_log (2026-10-11, Hall record standard): append-only log of policy changes, one row per PUT that changed something.
idBIGSERIAL PK,agent_idBIGINT,changed_atTIMESTAMPTZ default now,changesJSONB{field: {"from": old, "to": new}}(Decimals as strings, numerics compared without padding; a first policy lists the fields it sets, an unchanged save writes nothing). Written byput_policybefore the policy upsert, in the same transaction; no code path updates or deletes a row. Read byGET /torii/hall/{agent_id}(policy_changelog, last 10). Indexidx_nw_tw_agent_policy_log_agent (agent_id, id DESC). - nw_tw_idempotency (2026-10-12, console lane): Idempotency-Key store for the two agent writes (
POST /torii/mandates/{id}/events,POST /torii/transfers/report, and the MCP toolsmandate_report_event,transfer_report).scopeTEXT (<agent user id>:event:<mandate public id>or<agent user id>:transfer_report),idem_keyTEXT (1-128 chars of letters, digits and. _ : -),request_hashTEXT (sha256 of the request body without the key),statusINTEGER NULL andresponseJSONB NULL (both NULL while the first call is still running, filled when it finishes),created_atTIMESTAMPTZ default now. PK (scope,idem_key). Claimed withINSERT ... ON CONFLICT DO NOTHINGbefore the write, filled after it, deleted (only whilestatus IS NULL) when the write is refused or fails. A repeat with the same hash returnsresponse; a different hash isidempotency_key_reused. Rows are small and are never read by anything but the replay; no cleanup job yet (add one before volume). Created look-first bytorii_agent_wallet.ensure_tables, no ALTER on existing tables. - nw_tw_deposit_addresses:
agent_id,chain_id,address(the agent's own wallet on file, lowercase),created_at; PK (agent_id,chain_id). - nw_tw_transfer_reports:
idBIGSERIAL PK,agent_id,chain_id,asset,amountNUMERIC(38,6),tx_hash(lowercase),directionin/out,reported_by('agent' | 'chain'),reported_at,confirmed_at,statusreported/confirmed/failed,override_reasonTEXT NULL (set only when an outbound report overdrew idle under the owner'sauto_vs_approval.allow_overdraft; look-first column step, rails lane). UNIQUE (chain_id,tx_hash) so a duplicate report answers 409. An outbound report larger than idle for that chain/asset answers 409insufficient_idleand writes nothing. A freshinreport writesin_transit +amount; a freshoutreport writesin_transit +amountandidle -amountin one transaction (report_balance_deltas); a chain confirmation (confirm_deltas) movesin_transit -reported(inbound alsoidle +actual). - nw_tw_balance_events: append-only ledger.
idBIGSERIAL PK,agent_id,chain_id,asset,stateidle/reserved/in_transit/delivered,deltaNUMERIC(38,6) (signed),ref_kind,ref_id,created_at. Balances = SUM(delta) per (chain_id,asset,state).reservedis written bysvc/common/torii_reserved.pyfrom Auction House bids:ref_kindauction_bid(+notional,ref_id=nw_auction_house_orders_bids.id),auction_bid_close(−released,ref_id<bid id>:<event>),auction_bid_delivered(delivered+, same ref). AssetUSDT, chain = the auction chain. - nw_tw_deposit_watch_state:
chain_idPK,last_blockBIGINT,updated_at. Written in the same transaction as the poll's ledger rows; not written in dry-run (TW_DEPOSIT_WATCH_DRY_RUN=1, the default).