# Mandate > Version 1. A company-of-one travel founder lets an agent ask to pay hotels, airlines, and rides in USD. Mandate enforces caps, allow-list, hold, and Freeze. The agent cannot sign. Not an FCK vault, a TEE, or a scam registry. Dry quotes. submitted false. No invented transaction. This file is the agent contract. Humans can read the same rules at /docs. The Mandate gate on POST /api/v1/pay is the hard lock (caps, allow-list, holds). Jailbreak fields approved/policy are ignored. Not a TEE. Not a broadcast. ## Pages - [Home](/): Version 1. Company-of-one travel desk. One path to the console. - [Use case](/use-case): who it is for. The same job, not a second product. - [Docs](/docs): the same rules, written for a person. - [Console](/console): Run travel agent. Allow, hold, or block. Not an agent endpoint. ## Call only this POST /api/v1/pay ```json { "amountUsd": 8, "asset": "USDC", "to": "vendor.near", "chain": "base", "purpose": "API invoice 14", "idempotencyKey": "invoice-14" } ``` chain is one of base, ethereum, arbitrum, solana, polygon, near. asset is USDC. amountUsd is a number or a decimal string, at least 0.01, at most 2 decimal places, at most 1000000. to is plain ASCII: a .near account, a 20-byte hex address, or a base58 address. .near is matched without case. Do not send approved, policy, signature, or a private key. Those fields are ignored. A 200 response is a decision, not a payment. Read receipt.status. - allow: inside the policy. settlement.submitted is false unless a later integration broadcasts. - needs_approval: over the human line, still inside the caps. Wait. Do not retry with a higher amount. The amount is held. The balance has not moved. - reject: blocked. receipt.rule names the rule. - cancelled: a human rejected the hold, the UTC day changed, or the wallet was frozen. 409 error no_policy: the human has not saved a mandate. Do not invent one. On Vercel (local-db-ephemeral) a cold isolate and cross-isolate curl often return this even after a browser Run — expected. Prefer /console for the four-beat demo; curl against local npm run dev after Reset. 409 error conflict: this idempotencyKey was already used for a different payment. 400: the body itself is invalid. ## MCP tools (same gate) MCP is another face on this gate. Not a second policy. Not custody. Not a TEE. GET /api/mcp lists the tools. POST /api/mcp is JSON-RPC 2.0 (protocol 2024-11-05). Methods: initialize, tools/list, tools/call, ping. Local: npm run dev, then POST http://127.0.0.1:8080/api/mcp. Preview: POST https:///api/mcp. Do not use the old cursor/travel-desk-ux-512a host. - mandate_pay: arguments amountUsd, asset, to, chain, purpose, optional idempotencyKey. Same body as POST /api/v1/pay. Returns the decision receipt. settlement.submitted stays false. No transaction hash. - mandate_receipt: argument id from mandate_pay. Same as GET /api/v1/receipt?id=. 404 if this isolate forgot it. Outcomes inside the receipt: allow, needs_approval, reject, cancelled. Tool error no_policy with httpStatus 409: the human has not saved a mandate on this process. Do not invent one. conflict is 409. A bad body is 400. Policy, fund, and approvals are not tools. Do not call /api/v1/policy, /api/v1/fund, or /api/v1/approvals through MCP. ## Local judge path npm install, then npm run dev. Open http://127.0.0.1:8080/console. Leave Advanced closed. Run travel agent. Same five asks, Who can be paid (hotel.near, airline.near, rides.near), and demo-quote honesty as the preview: not sent yet, no payment record, or demo quote not attached. The screen says Demo quote · not sent yet. settlement.submitted stays false. npm run judge prints these steps and does not start the server. After Run, POST http://127.0.0.1:8080/api/v1/pay and POST http://127.0.0.1:8080/api/mcp hit that same process. Before rules are saved both return no_policy (409). npm test asserts src/lib/attack-suite.test.ts and src/lib/intents-fidelity.test.ts. There is no Makefile. ## Judge path (do X → see Y) 1. Open /console → Reset demo → Run the agent → four receipts → Approve $18 → scroll $8 (submitted false). 2. Jailbreak receipt lists ignoredFields approved, policy → Blocked allowed_recipients. 3. Travel desk (the primary path): idle desk shows Who can be paid chips hotel.near, airline.near, rides.near. A look-alike is a spelling check against that list, not a scam lookup. Run travel agent. Yes $80 hotel.near · Ask you $250 airline.near · No over the spending limit $500 · No fake look-alike hote1.near beside hotel.near (digit 1 vs letter l; Fake look-alike · not someone you can pay; approved ignored) · No for too many $120 rides.near when the day is full (earlier rides in that burst are Yes). A yes may show a NEAR demo quote. When that demo quote id is present, click or copy it. If the server returned none, the line says demo quote not attached. The screen says not sent yet and no payment record. settlement.submitted stays false. No transaction hash. Money is not sent. A tighter key is later, not in this demo: it cannot replace the spending limit, who can be paid, or ask-you-first. 15 cases with asserted verdicts (src/lib/attack-suite.test.ts and the line under the legend). Same request on the hotel row returns the same receipt and does not count twice. Money was not sent back, because nothing was sent. Freeze desk stops the desk in the demo. It does not pause the blockchain. Unfreeze is on that same bar. ## Read a prior receipt GET /api/v1/receipt?id= Same privacy-minimized receipt as POST /pay. 404 if Reset demo or a cold isolate dropped the ledger. Not a TEE. settlement.submitted stays false in preview. ## Human freeze If the human freezes the wallet, POST /pay returns reject with rule frozen. Agents cannot unfreeze. ## Do not call POST /api/v1/policy, POST /api/v1/fund, POST /api/v1/approvals, and GET /api/v1/policy are human routes. Without a server-side human token they return 403. Calling them is not a payment. ## Idempotency Send idempotencyKey, a string of at most 80 characters. A repeat of the same payment returns the original receipt and does not spend again. A repeat with a different amount, recipient, chain, or purpose is a conflict. Policy rejects are not stored, so a blocked ask does not use up the key log. If the settlement check cannot be reached, or answers 429 or 5xx, the result is settlement_unconfirmed and the same key can be retried. A dry-run with would_succeed false, or any other 4xx, is settlement_refused and is stored. Nothing was spent. If the check fails while a human is approving, the hold stays and the key still returns needs_approval. ## Privacy (minimize receipts) Default human policy privacyMode is minimize. On allow/reject/needs_approval receipts from this route: purpose is null and purposeHash is sha256 of the normalized purpose; to and settlement.recipient are truncated with recipientHash. Amounts, status, rule, and idempotency stay visible. You cannot set privacyMode in the body. Humans see full purpose and recipient on the console. This is not a TEE. settlement.submitted stays false. local-db is not encryption-at-rest. ## What this preview will not do It will not give you a private key. It will not treat the model as authorization. It will not broadcast. Durability (honest): local-db = file-backed PGLite that can survive a local process restart; local-db-ephemeral = in-memory PGLite on Vercel/serverless (warm isolate only — a cold isolate returns 409 no_policy / receipt 404); process-memory = persist skipped. None of these is a TEE or multi-instance shared store. Cross-isolate shared mandates need OutLayer / shared Postgres (roadmap). Do not treat a local allow as a TEE attestation. A dry-run runs only when the server has an OutLayer key, and only an allow with would_succeed true is spent. settlement.submitted stays false.