Skip to content

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"}
FieldMeaning
nClause number. Receipts cite it
keyStable name
text_enThe sentence the owner saw
paramsMachine values (amount, pct, days, ends_at, items) for translation
enforced_byContract, Torii guard or Venue
statusenforced 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": {}}
kindMeaning
instructionThe owner's purpose note, with clauses 1 to 10 (last in a group)
researchA paid read (fee event with a resource)
verifyA pre-action check (note with payload.preview)
consentAn approval (note with payload.approval); pending steps are pinned on top
actionAn agent buy, sell or move, or a chain Executed with no agent report
outcomeThe chain's measured result next to an agent report, a resolve, a payout or a mark
funding, control, noteDeposits 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:

FieldMeaning
periodThe ISO week in UTC
worth {start, end}Worth at both ends
flowsDeposits and withdrawals
result {realized, estimated}estimated = worth change minus net deposits minus realized change
fees {by_type}Fees by type
allowed_vs_usedThe largest move and busiest day against the caps
breaches, stepsAttempts outside the limits; the steps taken
disclosuresCodes 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's nw_users.id as text (same number the agent wallet tables hold as BIGINT). POST /torii/mandates/{id}/accept sets it when NULL or equal.
  • Created by svc.common.torii_mandates.ensure_tables(cur) (probe-first via svc/common/schema_ensure.py; complete schema sends no DDL; no ALTER on any request path). Not yet listed in the schema_ensure operator CLI.
  • nw_tw_mandates: one row per mandate. id BIGSERIAL PK; public_id TEXT UNIQUE (m_ + 10 base32 chars, the only id the API shows); owner_user_id BIGINT (nw_users.id, never returned by the API); agent_id TEXT NULL (the agent's nw_users.id as text; the agent role is the X-NW-User-Key whose user id equals it; set by the owner at create or by POST /torii/mandates/{id}/assign on a draft, never first-come); open_board BOOLEAN NOT NULL DEFAULT false (added 2026-10-11; the owner's explicit opt-in: only when true may any agent accept a mandate whose agent_id is NULL; designating an agent sets it back to false); purpose TEXT; budget_usdc NUMERIC(18,6); horizon_days INT; stop_floor_bps INT (floor = budget x (1 - bps/1e4)); target_bps INT NULL; payout_rule JSONB (default {"kind":"share_of_verified_gain","bps":1000}); allowed_targets JSONB list (default contract <mandate vault> + {"kind":"x402"}; a legacy {"kind":"caps",...} entry is read only when the new columns are NULL); per_tx_cap_usdc NUMERIC(18,6) NULL, daily_cap_usdc NUMERIC(18,6) NULL, approval_above_usdc NUMERIC(18,6) NULL (NULL = acts on its own inside the limits; stored, enforcement pending), on_floor TEXT NOT NULL DEFAULT 'keep' ('keep' | 'close'; stored, duty pending) (all four added 2026-10-11, lane 1 guardrails editor; per_tx/daily NULL = legacy row, the factory call falls back to the caps entry then the whole budget); chain_id INT default 84532; vault_address TEXT NULL; status TEXT CHECK in draft, signed, working, stopped, settled, cancelled; created_at, signed_at, ends_at TIMESTAMPTZ. Indexes: (owner_user_id, id DESC), (agent_id). last_chain_refresh_at TIMESTAMPTZ NULL (added 2026-10-12 by the schema step last_chain_refresh_at; set by svc/worker/nw_tw_vault_poll.py each 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_id TEXT NULL (added 2026-10-12; set only when the owner's own agent proposed the draft through MCP mandate_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. id BIGSERIAL PK; mandate_id BIGINT FK; kind TEXT (signed, deposit, move, buy, sell, fee, resolve, stop, withdraw, payout, note); amount_usdc NUMERIC(18,6) NULL (positive magnitude; resolve may be negative); tx_hash TEXT NULL; chain_id INT NULL; payload JSONB (position, mark, marks, target, next_at, text; a rule break adds violation: {codes:[...], detail:{...}}, written only by the server); reported_by TEXT (owner, agent, chain, keeper); created_at. Index (mandate_id, id). Partial unique index uq_nw_tw_events_chain_log on (mandate_id, LOWER(tx_hash), payload->>'log_index') WHERE reported_by='chain' and both are set (audit B2 N2: one vault log is one row; the chain reader inserts with ON CONFLICT DO NOTHING; server-written chain notes carry no log_index and 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 with invalid_number otherwise); a stored value that is not usable reads as 0 and the progress carries data_issue: true.
  • nw_tw_chain_cursor (2026-10-12, torii_mandates schema owner, not required by the API): where svc/worker/torii_vault_events.py resumed last. name TEXT PK (torii_vault_events); last_block BIGINT NOT NULL (the last block whose rows are committed; only ever moves forward); updated_at TIMESTAMPTZ. Created by python -m svc.common.schema_ensure torii_mandates or by the worker's own ensure_tables; nothing on a request path touches it.
  • nw_tw_mandate_events refused attempts (2026-10-12): a refused call is a note row whose payload.refusal is {reason, source, reason_source: "agent", check, attempted?}. reason is one of TargetNotAllowed, OverPerTxCap, OverDailyCap, MandateStopped, MandateEnded, server_guard. reported_by is chain only when the receipt proves a reverted call from the agent's wallet to the vault, else agent. The payload carries no block_number or log_index on purpose, so such a row never moves the chain reader's resume point. At most 60 per mandate per day; one row per tx_hash.
  • Facts, not conclusions: no profit column. worth_est, realized, floor, leave_now_est are derived on read by compute_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 with payload.violation and flips the matching rules[].ok to 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-first ensure_tables, no hot-path ALTER; operator apply: python -m svc.common.schema_ensure --apply torii_agent_wallet, also still python -m svc.common.torii_agent_wallet). agent_id convention (glue lane): every agent_id column and API field in both Torii Wallet sections is the agent's nw_users.id (the user id its X-NW-User-Key resolves to), a BIGINT here and a string in JSON; it is NOT nw_agents.id. The owner is found via nw_agents.owner_user_id joined on nw_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. Routes svc/api/routes/torii_agent_wallet.py, watcher svc/worker/nw_tw_deposit_watch.py. No exchange key or secret is ever stored; bodies carrying api_key/api_secret-style fields are refused (400).
  • nw_tw_agent_policies: one row per agent. agent_id BIGINT PK (= nw_users.id of the agent account), owner_user_id BIGINT, assets/venues/networks/stop_conditions JSONB (stored verbatim), per_tx_cap_usdc/total_cap_usdc NUMERIC(38,6), hold_ratio JSONB, min_net_bps/max_cost_bps/slippage_bps INTEGER, cherry_budget_daily BIGINT, auto_vs_approval JSONB, accepts_mandates BOOLEAN NOT NULL DEFAULT false (added 2026-10-11; true = the agent can be designated in a mandate and is listed by GET /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. id BIGSERIAL PK, agent_id BIGINT, changed_at TIMESTAMPTZ default now, changes JSONB {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 by put_policy before the policy upsert, in the same transaction; no code path updates or deletes a row. Read by GET /torii/hall/{agent_id} (policy_changelog, last 10). Index idx_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 tools mandate_report_event, transfer_report). scope TEXT (<agent user id>:event:<mandate public id> or <agent user id>:transfer_report), idem_key TEXT (1-128 chars of letters, digits and . _ : -), request_hash TEXT (sha256 of the request body without the key), status INTEGER NULL and response JSONB NULL (both NULL while the first call is still running, filled when it finishes), created_at TIMESTAMPTZ default now. PK (scope, idem_key). Claimed with INSERT ... ON CONFLICT DO NOTHING before the write, filled after it, deleted (only while status IS NULL) when the write is refused or fails. A repeat with the same hash returns response; a different hash is idempotency_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 by torii_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: id BIGSERIAL PK, agent_id, chain_id, asset, amount NUMERIC(38,6), tx_hash (lowercase), direction in/out, reported_by ('agent' | 'chain'), reported_at, confirmed_at, status reported/confirmed/failed, override_reason TEXT NULL (set only when an outbound report overdrew idle under the owner's auto_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 409 insufficient_idle and writes nothing. A fresh in report writes in_transit +amount; a fresh out report writes in_transit +amount and idle -amount in one transaction (report_balance_deltas); a chain confirmation (confirm_deltas) moves in_transit -reported (inbound also idle +actual).
  • nw_tw_balance_events: append-only ledger. id BIGSERIAL PK, agent_id, chain_id, asset, state idle/reserved/in_transit/delivered, delta NUMERIC(38,6) (signed), ref_kind, ref_id, created_at. Balances = SUM(delta) per (chain_id, asset, state). reserved is written by svc/common/torii_reserved.py from Auction House bids: ref_kind auction_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). Asset USDT, chain = the auction chain.
  • nw_tw_deposit_watch_state: chain_id PK, last_block BIGINT, 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).