Connect#
Get a key, find the door, call tools. English only. Testnet (Base Sepolia, chain 84532).
The short machine version is
/tw/llms.txt(also/tw-llms.txt). The one-page bootstrap isGET /torii/skill.md, generated from the live tool list and refusal registry.
What you are#
A person gives you a mandate instead of a key: a budget in USDC, a per-action cap, a daily cap, a stop line, an end date and a fee. A vault contract enforces them. You act inside it. You can never withdraw. Torii Wallet never asks for, stores or accepts an exchange key; a request body that carries a key or secret is refused.
Is the door open?#
The door is GET /torii/door, GET /torii/skill.md, GET /torii/mcp-tools and the mandate_* tools on POST /mcp?profile=full. A host opens them with one switch, TW_DOOR_ENABLED. While it is off they answer like a path that does not exist (HTTP 404, or an MCP "not available" error). A 404 on /torii/door therefore means this host has not opened the door yet; it is not a bad key.
Steps#
- Knock on the door.
GET /torii/doorneeds no key. It always answers HTTP402with the headerX-Torii-Door: mandateand a JSON body:mandate_schema,start(seven numbered steps, each with its tool and HTTP twin, and a readymandate_proposeexample),refusals(the API codes and the contract reverts),rate_limits,connect(the config blocks below) andnext. Nothing is charged;acceptsis an empty list. - Get a key.
POST /auth/agent/connectreturns anapi_key. Send it as headerX-NW-User-Key. Your agent id is the user id that key resolves to, as a string (GET /torii/agents/meshows it). It is notnw_agents.id. - Call tools. MCP over HTTP:
POST /mcp?profile=full(JSON-RPCtools/list,tools/call) with the same header. Every tool has an HTTP twin, exceptmandate_propose, which is MCP only. - Find your mandate.
mandate_offers(HTTPGET /torii/agents/{agent_id}/offers) lists mandates your owner designated to you. Accept one withmandate_accept. See Offers and accept. - Report.
mandate_report_eventfor what you did;mandate_report_refusalfor a call the vault reverted. See Reporting events. - Read
skill.md.GET /torii/skill.mdis publictext/markdown, under 150 lines, and carries the same config blocks as this page.
Client config#
Replace <api-host> with the host you were given and <your agent key> with your key. The same blocks come back in the door body (connect) and in skill.md, with the host filled in.
Claude Code (.mcp.json, or claude mcp add --transport http torii-wallet <url> --header "X-NW-User-Key: <key>"):
{
"mcpServers": {
"torii-wallet": {
"type": "http",
"url": "https://<api-host>/mcp?profile=full",
"headers": {
"X-NW-User-Key": "<your agent key>"
}
}
}
}
Claude Desktop (claude_desktop_config.json; it reaches a remote server through the mcp-remote bridge, which needs Node):
{
"mcpServers": {
"torii-wallet": {
"command": "npx",
"args": [
"-y",
"mcp-remote",
"https://<api-host>/mcp?profile=full",
"--header",
"X-NW-User-Key:${TORII_AGENT_KEY}"
],
"env": {
"TORII_AGENT_KEY": "<your agent key>"
}
}
}
}
Any other MCP HTTP client: one POST endpoint, JSON-RPC 2.0 (initialize, tools/list, tools/call), one header.
{
"url": "https://<api-host>/mcp?profile=full",
"method": "POST",
"headers": {
"Content-Type": "application/json",
"X-NW-User-Key": "<your agent key>"
},
"protocol": "JSON-RPC 2.0: initialize, tools/list, tools/call"
}
Plain HTTP with curl, the whole path in order:
# 1. knock (HTTP 402 is the door speaking; nothing is charged)
curl -i https://<api-host>/torii/door
# 2. get a key (send it as X-NW-User-Key from here on)
curl -X POST https://<api-host>/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-host>/torii/agents/me -H "X-NW-User-Key: $KEY"
curl -X POST 'https://<api-host>/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-host>/torii/agents/<agent id>/offers -H "X-NW-User-Key: $KEY"
curl -X POST https://<api-host>/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-host>/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-host>/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"}'
Key scopes#
The owner can narrow a key to scopes. A person's own account is not scoped.
| Scope | Covers |
|---|---|
read | Every GET, plus mandate_list, mandate_get, mandate_offers |
tx | Reporting events and refusals, accepting, stopping, policy writes (owner only), transfer reports |
sign | The owner's vault-creation report |
A missing scope is refused with reason: scope_denied. Scopes only narrow: the kill switch and Torii's caps always apply on top.
Rate limits#
- 2000 requests per key per day for a key with no plan (more on a plan). Past it:
429,reason: rate_limited, withretry_at. The counter is per API process and rolls over at 00:00 UTC. - 60 refusal reports per mandate per day. The same
tx_hashtwice counts once. - Writes you may retry carry an
Idempotency-Key; see Reporting events.
MCP#
Fifteen tools carry the family. They are listed by tools/list?profile=full and callable by name from any profile once the door is open; they are not in the default connector list.
| Tool | Hint | Notes |
|---|---|---|
mandate_list, mandate_get, mandate_offers, mandate_approvals | read-only | Safe to auto-allow. mandate_approvals lists the owner's approval items for actions you asked Torii to build above the approval amount |
agent_policy_get, agent_balances | read-only | Safe to auto-allow |
mandate_execute_call, mandate_unwind_call | read-only | Build calldata only; you sign and send. Above the owner's approval amount mandate_execute_call answers pending_approval and builds nothing; see Limits discovery |
mandate_propose | write | Creates a draft for the owner; moves no money |
mandate_accept, mandate_report_event, mandate_report_refusal, mandate_stop, transfer_report | write, idempotent | Safe to repeat; see Reporting events |
agent_policy_set | write, destructive | Replaces the whole policy; the owner's key only |
GET /torii/mcp-tools returns the same list as JSON. Reads and writes are separate tools: no tool both reads and writes. MCP answers a refusal as {error: {reason, message, retry_safe}}.
Refusals you can meet#
API refusals carry nightwatch.reason; the full table is in Refusals. The vault itself can revert an execute; these are the names mandate_report_refusal takes as reason:
reason | Meaning |
|---|---|
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 |
The door at 402#
A 402 from /torii/door is not a payment request. It is the front door speaking: it names the schema, the steps and the next calls and charges nothing. Read the body and follow start.
Per-client recipes#
Claude, Grok Bot and OpenAI (verified), Script (not verified yet).