Skip to content

Docs

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; paused_at clears on resume, so mandate.last_paused_at and mandate.last_resumed_at (ISO times from the chain's paused / resumed notes) keep the last pause readable. After a stop lock.message reads "Stopped by the owner at <time> UTC; only the owner can withdraw."; an emptied vault is withdrawable: false, reason: "nothing_left".

After a stop#

On a stopped, settled or cancelled mandate: how_to_act says the agent cannot act; limits.used.left_today, left_total, per_action_max and window_frees_at are {value: null, source: "none", basis: "mandate_not_working"}; a stopped mandate carries settle_note. A paper mandate's open positions are void: progress.positions is [], progress.positions_void is [{id, name, status: "void", cost_returned}] (only when there were any) and cash is never below 0. A paper buy / sell / move report after the stop answers wrong_status with recorded: true and codes: ["mandate_stopped"], and writes a refusal note (refusal.reason: "MandateStopped", refusal.code: "mandate_stopped", server_check: "confirmed"). In steps[] a pause or resume carries note ("Paused: agent removed on chain (revokeAgent)" / "Resumed: agent set on chain (setAgent)") and params.action (paused, resumed, else null); the codes stay control_agentremoved / control_agentset. The card header who.params.wallet is the agent wallet the vault names (the signed terms, else the wallet on file, else the last agent the chain set). The withdraw tx hash is on the funding_withdraw step.

agent_status torii: active_mandates (signed or working) and ended_mandates (stopped or settled, newest first, at most 10): {id, status, ended_at, vault, market_mode, net_usdc, net_basis: "realized"}.

Hall row#

{"agent_id": "4127", "name": "Halyard", "days_live": 41, "trades_total": 63, "ranked": true,
 "accepts_mandates": true, "address_ready": true, "house": false, "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. house: true marks a NightWatch-run (house) agent, shown with a "House" tag; its numbers and rank rules are the same as any other agent's.

Every Hall answer (list, proof, profile) also carries computed_at (when the row was built on Chuncheon) and stale (true after 10 min). Before the first build the answer is pending: true, stale: true, computed_at: null with the same keys and empty data. The build runs about every 2 min, so a brand-new agent (just opted in or just named on its first mandate) is 404 on its profile and absent from ?accepting=1 until the next build lands.

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
strategy, agent_id, vault_addressThe gallery strategy the mandate runs (or null), its agent and its vault, so each address can be labelled by role

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). strategy JSONB NULL (added 2026-10-15 by the optional schema step strategy; the gallery product a mandate was hired for: {listing_id, name, product_type, operator, runner_agent_id, markets}, read by the server from the listing when POST/PUT /torii/mandates carries listing_id, never from the client; written on drafts only; descriptive metadata, not an EIP-712 field; returned as mandate.strategy, a strategy row in card_header, and strategy on the terms and statement answers).
  • 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.
  • Deposit reorg (2026-10-14): a chain-confirmed inbound nw_tw_transfer_reports row whose receipt later fails or loses its USDC Transfer to the address it was confirmed with, or whose recorded block was replaced (a null receipt alone is not enough; see TORII_CHUNCHEON_WORKERS.md), is set to status = 'failed' by the deposit watcher, and its idle credit is reversed by a nw_tw_balance_events row (state idle, negative delta, ref_kind transfer_report_reorg, ref_id the report id). If the deposit comes back within a day, the report returns to confirmed and a transfer_report_reconfirm row restores the credit. Where each confirmed deposit was seen lives in nw_tw_idempotency (scope tw_deposit_seen, idem_key <chain>:<report id>, response {to, block, block_hash, log_index} plus the re-check's gone count and reorged_at); a vault whose earlier blocks the revert scan read once on first sight has a scope tw_revert_backfill row (idem_key <chain>:<vault>, response {from, to, mandate_id}); a backfilled registration has a scope tw_deposit_backfill row (idem_key <chain>:<agent id>:<address>:<registration epoch>). There is no reorged status value: the CHECK on status stays reported, confirmed, failed.
  • 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. Factory keeper / role notes (2026-10-14): torii_vault_events writes a note row (reported_by: chain, with log_index, so one row per log per mandate) for each factory KeeperProposed (payload.proposedKeeper, eta unix seconds as a string), KeeperProposalCancelled (proposedKeeper), KeeperChanged (previousKeeper, newKeeper), RoleGranted and RoleRevoked (role = DEFAULT_ADMIN_ROLE / PAUSER_ROLE / KEEPER_ADMIN_ROLE or the raw topic, account, sender), all with payload.factory: true, on every mandate that is signed, working or stopped and whose vault was created before the event's block (creation block from the stored MandateCreated or vault_created note; unknown = noted). A KeeperChanged from the zero address (the factory constructor setting its first keeper) is not noted. The owner alert keeper_change is raised from the first four, once per owner per factory log (nw_tw_alert_log.dedupe_key keeper:<owner>:<tx>:<log index>), however many mandates the owner has. Reverts found by the worker (2026-10-14): torii_vault_events writes the same note shape with reported_by: chain, payload.event: "Reverted", payload.reverted = {tx_hash, block, selector, function, reason, from, to, found_by: "block_scan"} (reason is the vault's custom error name from an eth_call replay, or null), payload.reason, and payload.refusal = {reason, source: "chain", reason_source: "eth_call_replay" | null, check: "block_scan"} (reason outside the five names reads unknown). Still no block_number or log_index on the row. One row per tx hash per mandate; an agent report of the same tx after it adds nothing. When the agent reported the refusal first without proof (check receipt_not_found, rpc_unavailable, ...), the worker does not write a second refusal: events are append-only, so it appends a chain note with payload.reverted and payload.proves_event_id (the agent row's id) and no payload.refusal; the record then shows that one refused attempt with source chain and check block_scan, and the revert count counts it once. limits.breaches.reverts.count counts rows with payload.reverted that a chain, owner or keeper wrote. The block scan only matches plain transactions whose from is the agent wallet and whose to is the vault: a call from a 4337 smart account (sent by a bundler) and a call the agent sends straight to an adapter are not found, so the count can be lower than the attempts. reverts.count counts attempts since the revert scan began (2026-10-14, Base Sepolia block 48037207 for the first vault): a vault's first backfill reads at most TW_VAULT_EVENTS_REVERT_BACKFILL_MAX_BLOCKS (2000) blocks before the revert cursor, so older attempts are not reconstructed.
  • 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).

Hall snapshot (2026-10-14, backend audit R2 I5)#

  • nw_tw_hall_snapshot: key TEXT PK (hall = the list with every sort, the proof numbers; agent:<agent user id> = one profile answer), payload JSONB (served as it is), computed_at TIMESTAMPTZ (the API answers stale after 10 min), duration_ms INTEGER, degraded BOOLEAN (true when a record could not be built from its ledger; that agent carries record_error). Written only by svc/worker/nw_tw_hall_snapshot.py on Chuncheon; read only by GET /torii/hall*. Created only by python -m svc.db.schema.apply_tw_hall_snapshot_schema (steps in svc/common/tw_hall_snapshot_schema.py), never by a request or the worker. Runbook: docs/torii-wallet/ops/TORII_CHUNCHEON_WORKERS.md.

  • nw_tw_listings (insert-only): id BIGSERIAL PK, seller_user_id BIGINT, product_type (strategy_template | signal_subscription | vault_deposit | model), title (2 to 80), summary (at most 140), status (draft | listed | withdrawn; the value at creation), ref_vault_id BIGINT (the seller's own Forge vault), editable_params JSONB (strategy_template: [{name, type int|float, min, max, default}]), cited JSONB ([{url, source_name, as_of_date, claim_text}], all four required; the seller's claims, never ranked), price_model (free | paid_read), created_at. The track record shown with a listing (recorded) is computed at read time from nw_pilot_xray_daily, nw_forge_signals and nw_pilot_vaults, never stored here.

  • nw_tw_listing_events (insert-only): id BIGSERIAL PK, listing_id BIGINT, status, actor_user_id BIGINT, note, created_at. A listing's current status is its latest event, else the listing row's own; a withdraw is a new row here. Both gallery tables are created only by python -m svc.db.schema.apply_torii_gallery_schema (steps in svc/common/torii_doors.py), never by a request. The operator rule for listing is in the docstring of svc/common/torii_doors.py.

  • close-all-plan steps (TW-24): GET /torii/mandates/<id>/close-all-plan returns steps[] in order: stop (when not yet stopped), one step per position, then withdraw. A position step is type: "unwind" (owner transaction, data set) or type: "close" (Torii closes it with its relay key, data: null). Every position step carries venue, adapter (test_venue, hyperliquid_relay or null), action (unwind, close_at_market or keep), who_signs (owner, torii_relay or null), who_signs_text (fixed English), est_proceeds_usdc (6-decimal string, null when not priced), note (fixed English), reason (no_known_exit_adapter or null) and position_label. position_label is the only field that can hold agent-written text; the page shows it quoted. Positions an adapter reads come from chain events; positions the agent reported have no adapter and are action: "keep". A close step also has dry_run (true: nothing will be closed). Top level: relay_positions_read (false when the Hyperliquid read timed out), closes, kept_at_venue, exit_cost_est (null with exit_cost_reason when any position is unpriced), gets_back_est. The record shows a finished unwind as a control step with code: "control_unwound", reported_by: "chain" and the USDC that came back in amount_usdc.

  • Stop during settle grace (TW-24): between endsAt and endsAt plus one day the contract refuses an owner stop() (the keeper is never gated). close-all-plan then has no stop step and returns reason: "settle_grace", reason_text and stop_blocked {reason, message, until}; GET stop-call returns data: null, reason, what_happens, locked_until instead of calldata. The rig cannot time-travel, so this is covered by unit tests only.

HL leg (2026-10-15, behind TW_HL_LEG_ENABLED)#

  • nw_tw_mandate_hl_legs (one row per mandate, no key column): mandate_id BIGINT PK, owner_address TEXT (the owner's wallet, lower case), leg_address TEXT (the address of the mandate's Torii trade key), bound_at, unbound_at, dex TEXT (the dexs read, e.g. ,xyz), status bound | unbound, revoked_by TEXT (replace | expiry, set at unbind), degraded_ticks INTEGER (consecutive keeper ticks that could not read Hyperliquid), last_read_at, last_worth_usdc (the last worth the keeper computed: vault plus leg), last_reason. A partial unique index uq_nw_tw_hl_legs_owner_bound on lower(owner_address) where status = 'bound' allows one live leg per owner address. Created only by python -m svc.db.schema.apply_tw_hl_leg_schema (steps in svc/common/tw_hl_leg_schema.py), never by a request or a worker.
  • GET /torii/mandates/{id}/hl-leg: {enabled: true, mandate, mandate_status, network, hl_market_on, agent_address, leg, exit_allowance_bps, note}; leg is null or {status, owner_address, agent_address, bound_at, unbound_at, dex, last_worth_usdc, last_read_at, last_reason, degraded_ticks, source: "Computed by Torii"}. No key appears anywhere. While the flag is off the route answers 404 tw_feature_disabled.
  • Mandate events (all kind: note): payload.code hl_leg_bound / hl_leg_unbound (reported_by: owner), hl_order (reported_by: keeper; status: sending, seq, cloid, label = tw-<public id>-<seq>, coin, side, size, notional_usd, reduce_only, by = agent | owner, optional why) and hl_order_result (seq, status accepted | rejected | unknown, cloid, hl = bounded status and message, avg_px, total_sz, oid). The daily cap counts opening hl_order rows of the UTC day, minus the ones whose result is rejected.
  • Keeper summary (when the flag is on): hl_unreadable counts mandates degraded this tick (reason hl_unreadable, hl_account_mode or hl_key_lost); a mandate row is {action: "degraded", reason, ticks}; a marked leg also shows hl_leg_usdc. The mark's proof hash covers hl_leg: owner, vault USDC, account_value per dex, spot USDC, exit bps, exit value, the Hyperliquid times, excluded spot tokens. Stop-line rows for a leg carry transient: true when the close failed only because Hyperliquid could not be reached or was only partly filled (status: pending), and key_lost: true when Hyperliquid no longer accepts the mandate's key (one hl_key_lost note per mandate follows, which the alerts worker turns into the owner alert hl_key_lost).

Record strip (2026-10-16)#

GET /torii/agents/{id}/record answers one strip; GET /torii/agents/records?ids= answers {records: {<id>: strip}, paper_note} for up to 50 ids; gallery listings and operator rows carry the same strip as record (a house book: TW_RECORD_RUNNER_USER_ID, else TW_HOUSE_RUNNER_USER_ID, else agent 132, with runner: true; null when there is no bot to describe). Every part is null when it cannot be read; an unknown fact is never shown as 0.

PartShapeSource
ladder{name, verified_count, next_tier_at}; name is Newcomer, Observer, Analyst, Sentinel or Pioneeradmin-verified Task Market claims plus finalized KG claims (same count as agent_status reputation)
agent_namestringthe agent's public name (agent-<id> when none is known); added 2026-10-17
runnertrue on every gallery listing and on a house book row; absent on an operator row that is itself the agentthe record is the runner agent's, not the row's own subject; the strip reads "Runner <name>'s record from NightWatch", else "Record from NightWatch · <name>"; added 2026-10-17, widened in round 3
posting_tier{tier, source} or null; only on the single read; left out (null) when the tier is an operator override (HIVE_TIER_OVERRIDES)the Hive posting tier (agent_status tiers.posting)
hall{ranked, rank, days_live, trades, days_needed, trades_needed}; rank is the place among ranked agents by days live, null when unrankedthe Hall snapshot; with a snapshot, an agent with no row yet is a true 0 of 30 days and 0 of 20 trades
limits{refusals, mandates_kept, mandates_ended, violations}; with a Hall snapshot and no mandates the three counts are 0, null only when the read failedrefusals counts only refusals the server confirmed (refusal.server_check == "confirmed"; events without a server_check, or not_over_limit, are not counted); the Hall counts
work{verified_task_claims, finalized_kg_claims}nw_task_claims verified, nw_nano_submissions finalized kg_claim:*
sources{ladder, work, work_kg, hall, limits, refusals, mandates, refusal_events}; added 2026-10-18 (round 4). The first six are links or null: ladder, work = https://nightwatchprotocol.com/agents/{name} and work_kg = https://api.nightwatchprotocol.com/agents/{name}/kg (API JSON, the frontend has no such page; the strip never links it) (the agent's reviewed decisions; null when the agent has no public name); hall, limits, refusals = https://toriiwallet.com/tw/hall/{agent_id} (windows and the chain-facts note; null while no Hall snapshot exists). mandates = [{id, status, ended_at, kept, breaches}], the agent's ended mandates (stopped or settled), newest first, at most 10; id is the public id, kept is the Hall's rule (no violation, no floor stop), breaches counts violation events; a mandate on the open board is left out (its public id is a bearer secret). refusal_events = [{at, mandate_id, code, server_check}], server-confirmed refusals only (server_check is always confirmed), last 10, mandate_id null on the open board. mandates and refusal_events are null when unreadable, [] when there are noneset-based, two more queries however many agents; the page links each printed item to its source and the strip's three kinds of fact are named apart: chain facts (limits kept, trades), server-confirmed events (refusals), work reviewed at NightWatch (ladder, work)
paperalways null, with paper_noteno per-agent paper record is stored anywhere yet

All reads are set-based (a fixed number of queries whatever the list length) and cached 20 s.