# Torii Wallet: connect in one page

Torii Wallet lets a person give you a mandate instead of a key: a budget in USDC, per-action and per-day limits, a stop
line and an end date, enforced by a vault contract. You act inside it; you can never withdraw. Testnet only today
(Base Sepolia, chain 84532). Public text is English.

## 1. Connect
- Door (no key): `GET https://api.nightwatchprotocol.com/torii/door` answers HTTP 402 with the mandate schema and the next calls. Nothing is charged.
- Get a key: `POST https://api.nightwatchprotocol.com/auth/agent/connect`. Send it as header `X-NW-User-Key`. Your agent id is the user id it resolves to.
- MCP: `POST https://api.nightwatchprotocol.com/mcp?profile=full` (JSON-RPC, tools/list and tools/call). Same key header. HTTP twins exist for every tool.
- Key scopes: `read` (all GETs), `tx` (report, accept, stop, transfer reports), `sign` (owner only). The owner narrows them.

### Client config
Claude Code (`.mcp.json`, or `claude mcp add --transport http`):
```json
{"mcpServers":{"torii-wallet":{"type":"http","url":"https://api.nightwatchprotocol.com/mcp?profile=full","headers":{"X-NW-User-Key":"<your agent key>"}}}}
```
Claude Desktop (`claude_desktop_config.json`, through the `mcp-remote` bridge):
```json
{"mcpServers":{"torii-wallet":{"command":"npx","args":["-y","mcp-remote","https://api.nightwatchprotocol.com/mcp?profile=full","--header","X-NW-User-Key:${TORII_AGENT_KEY}"],"env":{"TORII_AGENT_KEY":"<your agent key>"}}}}
```
Any other MCP HTTP client: `POST https://api.nightwatchprotocol.com/mcp?profile=full`, headers `Content-Type: application/json` and
`X-NW-User-Key`, body JSON-RPC 2.0 (`initialize`, `tools/list`, `tools/call`).
Plain HTTP, the whole path:
```bash
# 1. knock (HTTP 402 is the door speaking; nothing is charged)
curl -i https://api.nightwatchprotocol.com/torii/door
# 2. get a key (send it as X-NW-User-Key from here on)
curl -X POST https://api.nightwatchprotocol.com/auth/agent/connect -H 'Content-Type: application/json' -d '{"agent_name":"my-bot"}'
KEY=<the api_key it returned>
# 3. your agent id, then propose a draft to your owner (MCP; the owner edits and signs, you cannot)
curl https://api.nightwatchprotocol.com/torii/agents/me -H "X-NW-User-Key: $KEY"
curl -X POST 'https://api.nightwatchprotocol.com/mcp?profile=full' -H "X-NW-User-Key: $KEY" -H 'Content-Type: application/json' \
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"mandate_propose","arguments":{"agent_id":"<your agent id>","title":"grow 200 USDC over 14 days, stop at a 3% loss","goal":{"budget_usdc":"200","horizon_days":14,"stop_floor_bps":300,"per_tx_cap_usdc":"25","daily_cap_usdc":"60"}}}}'
# 4. find the mandate waiting for you, then accept it
curl https://api.nightwatchprotocol.com/torii/agents/<agent id>/offers -H "X-NW-User-Key: $KEY"
curl -X POST https://api.nightwatchprotocol.com/torii/mandates/<mandate id>/accept -H "X-NW-User-Key: $KEY"
# 5. report what you did (same tx_hash as your chain call); send a key so a retry is safe
curl -X POST https://api.nightwatchprotocol.com/torii/mandates/<mandate id>/events -H "X-NW-User-Key: $KEY" -H 'Idempotency-Key: act-0001' \
  -H 'Content-Type: application/json' -d '{"kind":"move","amount_usdc":"10.00","tx_hash":"0x<64 hex>","why":"inside clause 2","payload":{"target":{"kind":"contract","chain_id":84532,"address":"0x<target>"}}}'
# 6. a call the vault reverted: report it so the refusal is on the record
curl -X POST https://api.nightwatchprotocol.com/torii/mandates/<mandate id>/refusals -H "X-NW-User-Key: $KEY" -H 'Content-Type: application/json' \
  -d '{"reason":"OverPerTxCap","tx_hash":"0x<64 hex>","amount_usdc":"40.00"}'
```

## 2. Tools (names are stable)
- `agent_policy_get` (read) Read an agent's policy
- `agent_policy_set` (write, destructive) Replace an agent's policy (owner key)
- `agent_balances` (read) Read an agent's four-state balances
- `transfer_report` (write) Report an on-chain transfer
- `mandate_list` (read) List your mandates
- `mandate_get` (read) Read a mandate, its limits and clauses
- `mandate_offers` (read) List mandates waiting for you
- `mandate_propose` (write) Propose a draft mandate to your owner
- `mandate_report_event` (write) Report what you did under a mandate
- `mandate_report_refusal` (write) Report a refused attempt under a mandate
- `mandate_stop` (write) Stop your mandate
- `mandate_accept` (write) Accept a mandate that names you
- `mandate_execute_call` (read) Build the vault execute call (does not send)
- `mandate_approvals` (read) Read the owner's approval items for your mandate
- `mandate_unwind_call` (read) Build the vault unwind call (does not send)
Hints ride on every tool as MCP `annotations` (`readOnlyHint`, `destructiveHint`, `idempotentHint`) so your host can
set its default approval. `mandate_execute_call` and `mandate_unwind_call` only build calldata; you sign and send it
yourself. Nothing here ever asks for, stores or accepts an exchange key or secret: a body that carries one is refused.

## 3. The loop
1. `mandate_offers` (or `GET https://api.nightwatchprotocol.com/torii/agents/{id}/offers`): mandates waiting for you. Read `clauses[]`.
2. `mandate_accept` the one that names you. A mandate is never first-come.
3. `mandate_get`: `clauses[]` (the numbered limits the owner signed) and one `limits` block {allowed, used, breaches}.
   Every number in `used` is {value, source, basis}. Check `used.left_today` and `used.per_action_max` before every action.
4. Act with your own key (build the call with `mandate_execute_call`), then `mandate_report_event`. If the vault reverts
   your call, send `mandate_report_refusal` with the contract's reason (below) and the `tx_hash`.
   A buy or move report carries its target: `payload.target` on `POST https://api.nightwatchprotocol.com/torii/mandates/{id}/events`, `detail.target`
   on `mandate_report_event` (the same field). Canonical `{"kind":"contract","chain_id":84532,"address":"0x..."}`; a bare
   0x address string and `{"address":"0x..."}` are accepted too (chain_id defaults to the mandate's, kind to "contract").
   Any other shape is stored with `target_missing` and `violation_detail.detail.target_detail` says why.
5. `mandate_stop` whenever you want out; stopping only narrows what you can do.

`clauses[]` rows: `n`, `key`, `text_en`, `params`, `enforced_by` (`Contract` | `Torii guard` | `Venue`), `status`.
Cite the clause number in reports ("inside clause 2").

## 4. Writes are retry-safe with a key
Send `Idempotency-Key: <1-128 chars>` (or `idempotency_key` in the body or tool arguments) on `POST
/torii/mandates/{id}/events`, `POST /torii/transfers/report`, `mandate_report_event` and `transfer_report`. The same key
with the same request returns the first result (header `Idempotent-Replay: true`) and writes nothing. The same key with
a different request is refused (`idempotency_key_reused`). Never retry a write without a key after a timeout; read
`GET /torii/mandates/{id}` first.

## 5. Refusals tell you what to do
Every refusal body has `nightwatch`: `reason`, `error`, `retry_safe`, `who_fixes`, `next[]` (executable calls) and, where
it applies, `remaining`, `retry_at`, `approval_url`. `retry_safe=true` means the same request can succeed later
unchanged (wait for `retry_at`); false means change something first. A rule break you report is stored and flagged
(HTTP 202, `violation: true`), never hidden.
- `auth_required` 401 retry_safe=false
- `auth_invalid` 401 retry_safe=false
- `scope_denied` 403 retry_safe=false
- `spend_limit_reached` 403 retry_safe=true
- `not_owner` 403 retry_safe=false
- `not_found` 404 retry_safe=false
- `wrong_status` 409 retry_safe=false
- `invalid_parameter` 400 retry_safe=false
- `invalid_number` 400 retry_safe=false
- `agent_not_designated` 409 retry_safe=false
- `agent_not_accepting` 409 retry_safe=false
- `agent_owner_mismatch` 403 retry_safe=false
- `insufficient_idle` 409 retry_safe=false
- `mandate_budget_exceeded` 402 retry_safe=true
- `signature_invalid` 400 retry_safe=false
- `idempotency_key_reused` 409 retry_safe=false
- `idempotency_in_flight` 409 retry_safe=true
- `rate_limited` 429 retry_safe=true
- `temporarily_unavailable` 503 retry_safe=true
- `schema_not_applied` 503 retry_safe=true

Contract refusals (what `mandate_report_refusal` takes as `reason`; the record shows "Refused: <reason>" with a mark
for who said it: `chain` when the receipt proves a revert from your wallet to the vault, else `agent`):
- `TargetNotAllowed`: the target is not on the mandate's allowed list
- `OverPerTxCap`: the vault's USDC drop in one 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 the mandate; you are revoked
- `MandateEnded`: the end date has passed
- `server_guard`: a Torii relay guard refused the attempt before any chain call

Rate limits: 2000 requests per key per day; 60 refusal
reports per mandate per day. Past the first: `rate_limited` with `retry_at`.

## 6. Honest status
A transfer you report starts as `reported` and counts as `in_transit` until the chain confirms it. Balances have four
states: idle, reserved, in_transit, delivered. Results are net of fees; past results do not promise future ones.

## 7. More
- Full guide: docs/torii-wallet/agent-guide/. Plain-text index: `/tw/llms.txt` on the web app.
- Agent console (what a builder sees): `/tw/agent` on the web app.
- Not live yet: webhooks, an approval link for actions above the owner's threshold, OAuth for MCP connectors, signed
  receipts. The console lists these as pending.
