Mandate
A helper can ask. It cannot send the money.
Version 1 is a one-person travel desk. You set spending limits, who can be paid (hotel, airline, and rides), when it must ask you first, and Freeze. Run travel agent on the desk: yes, ask you, and no — a fake look-alike and too many rides — in under a minute. This demo checks the rules and does not send money. There is no fake payment record. Freeze stops the desk in the demo. It does not pause the blockchain.
New here? Use the travel desk. The rest of this page is the longer write-up, and it keeps the technical names.
Tracks: AI Security & Defense · Agent Economy · Autonomous Business (NEAR HACKATHON 2026). Repo: github.com/arults/Mandate. Agent contract: /llms.txt · AGENT.md.
1. Abstract
One human-authored spending mandate. Agents propose payments over a single HTTP surface. A policy gate outside the model returns allow, needs_approval, or reject. Signing keys stay out of the agent (preview: local policy gate — not a TEE; production roadmap: OutLayer Agent Custody / NEAR MPC). Settlement path is NEAR Intents. Prompt injection cannot raise caps, swap recipients, or mark a payment approved.
2. Problem
- Agents that hold raw keys are steerable — prompt injection, poisoned tools, malicious 402 / MCP text.
- System-prompt “limits” are requests, not locks: untestable, no artefact, they degrade under attack.
- Cross-chain agent commerce needs Intents-class rails without giving the model a FullAccess key.
- Autonomous businesses need human constraints with machine speed under the line.
3. Thesis
Authorization is not a language-model problem. It is a gate problem. The model proposes {amount, asset, to, chain, purpose}; something that is not the model decides.
4. Threat model
| Adversary | Capability | Mandate control |
|---|---|---|
| Prompt-injected agent | Steers tool args | Gate recomputes; ignores approved / policy |
| Malicious merchant / tool text | Inflates amount or swaps payee | Allow-list + caps + approval line |
| Agent with code exec + raw signer | Bypass any in-process hook | Out of preview threat model; production requires out-of-process signer (OutLayer) |
| Operator mistake | Over-wide allow-list | Freeze; approval threshold; day limits |
Honest non-goals: not a full sandbox; not KYC; not antifraud ML.
5. Architecture
Human console → policy / fund / approve
Agent → POST /api/v1/pay only
↓
policy gate (not LLM)
↓
allow | needs_approval | reject
↓
receipt + Intents path (preview: not broadcast)Components: policy types · evaluatePolicy · gate.server · human routes · agent route · console · docs · llms.txt.
6. Policy object
Fields: maxPerTransactionUsd, dailyLimitUsd, allowedRecipients[], approvalThresholdUsd, allowedAssets (USDC), frozen.
Evaluation order (matches code): frozen → asset → chain → recipient → balance/holds → max/tx → daily remaining → approval threshold.
Policy document mapping (enclaveDocument): OutLayer-shaped rules (preview evaluates locally; not an attestation) + mandate_only.approval_threshold_usd + evaluated_by. Preview labels evaluated_by: "process-memory".
Demo mandate: $25 max per payment, $50 per day, approve above $10. Recipients vendor.near, studio.near, and one EVM address. Chains: base, ethereum, arbitrum, solana, polygon, near.
Travel ops mandate: $300 max per payment, $800 per day, approve above $150. Recipients hotel.near, airline.near, and rides.near. Same USDC asset and privacy minimize. Still not an attestation, and settlement.submitted stays false.
7. Agent interface
Call only POST /api/v1/pay. Full contract: /llms.txt.
{
"amountUsd": 8,
"asset": "USDC",
"to": "vendor.near",
"chain": "base",
"purpose": "API invoice 14",
"idempotencyKey": "invoice-14"
}Read receipt.status. A 200 is a decision, not a payment. Do not send approved, policy, a signature, or a key — those fields are ignored.
- allow — inside the policy. settlement.submitted is false in preview.
- needs_approval — wait. Do not retry with a higher amount.
- reject — blocked. receipt.rule is the reason.
- cancelled — human rejected, day changed, or wallet frozen.
409 no_policy: human has not saved a mandate. 409 conflict: idempotency key reused for a different body. 400: invalid body.
8. Human interface
The console owns policy, fund, and approve / reject. HTTP human routes require MANDATE_HUMAN_TOKEN.
Production path: publish the policy into OutLayer Agent Custody. The agent keeps a request key (wk_) only — it can ask, it cannot rewrite the mandate.
9. Settlement & NEAR
- Protocol: NEAR Intents / 1-Click. On allow, the gate may call POST https://1click.chaindefuser.com/v0/quote with dry: true and attach correlationId / amount on the receipt.
- Quote failures degrade: status unavailable, payment still allowed by policy. Quote never broadcasts.
- Optional OutLayer intents/withdraw dry-run; spend only if would_succeed: true.
- Preview: settlement.submitted = false always.
- Production path: OutLayer Agent Custody + Intents (partner docs).
10. Function Call Keys (security track)
NEAR Function Call Keys (FCK) limit a key to a receiver, methods, and allowance. They are not a USD policy engine: they cannot encode daily caps, human approval thresholds, or recipient allow-lists the way Mandate does. Live today is the off-chain gate plus a 1-Click dry quote. This page does not deploy an FCK and does not show a testnet transaction.
Composition (now documented; not a fake attestation):
- The human (or OutLayer enclave) keeps FullAccess. The agent never receives FullAccess.
- The agent holds only a Mandate / OutLayer request credential (wk_) to ask — and optionally an FCK scoped to the Mandate gate or OutLayer withdraw methods.
- Sub-account pattern: agent.<treasury>.near with an FCK; treasury FullAccess stays offline with the human (production: OutLayer custody — roadmap).
Roadmap: FCK issuance helpers + OutLayer TEE attestation verify page. This preview does not emit attestation measurements or claim enclave identity.
11. Safety properties
Claim only what tests cover (policy.test.ts · gate.security.test.ts · npm test):
- Jailbreak fields ignored.
- Caps cannot be overridden by a human-approval flag alone.
- Holds prevent day oversubscription.
- Idempotency conflict on body mismatch.
- OutLayer false dry-run does not spend.
- At most 20 payments can wait; UTC day change cancels pending and releases holds.
- HTTP 429 and 5xx can be retried; they are not stored as a no.
12. Limits of this preview
- It does not give the agent a private key.
- It does not treat the model, or a field named approved, as authorization.
- It does not broadcast a mainnet transaction or invent a solver fill.
- Default durability is local-db (embedded PGLite). That survives a process-memory clear / local restart. It is still not a TEE and not multi-instance shared storage. Set MANDATE_DURABILITY=process-memory for in-memory-only. Serverless cold starts without shared Postgres can still lose /tmp data.
- No TEE attestation unless an OutLayer dry-run returns would_succeed: true. Without a key, attestation kind is local-policy-gate.
- No marketplace, second agent, KYC, or fiat ramp.
13. Mandate family (roadmap)
| Product | One-liner |
|---|---|
| Mandate | Policy gate for agent spend (this submission) |
| Mandate Receipts | Audit seal of allow / reject receipts (roadmap) |
| Mandate Pool | Multi-agent shared daily budget + per-agent subcaps (roadmap) |
| Mandate Lists | Versioned allowlists humans subscribe to (roadmap) |
Same trust boundary. Different surfaces. Not shipped as products yet.
14. Hackathon demo protocol
Do X → see Y. Open /console, read the spending limits, click Run travel agent, then press Yes on the airline row. The $8 path is under Advanced.
- Under Advanced: Reset demo (optional clean slate), then Run the small demo. Saves the $8 rules. Left today $50. Demo balance $200. One click prepares and asks four times together (avoids a cold page returning “no rules”).
- $8 to vendor.near → Yes. The receipt says the rules were checked in this demo. Not sent yet. Left today $42.
- $18 to studio.near → Ask you. Today’s remaining room drops because the amount is waiting. The demo balance stays $200 until you press Yes. No drops the ask.
- $40 → No · over the spending limit.
- Fake yes → the ask claims you already said yes and invents a new limit. The receipt lists those as ignored. Status: No · not someone you can pay.
The desk is the trip. Limits on the page are $300 per payment, $800 today, ask you above $150. Who can be paid: hotel.near, airline.near, rides.near. Click Run travel agent. The five lines are yes, ask you, over the limit, a fake look-alike, and too many rides. Chips Yes, Ask you, Over limit, Look-alike, and Too many rides each play one ask. Look-alike highlights the wrong name. A yes row shows a demo quote: not sent yet, click or copy the demo quote id when one came back (otherwise the line says demo quote not attached), and no payment record. Load hotel, airline, and rides only fills the editor under Advanced. The $8 path stays on Run the small demo there too. Extra checks: 15 cases, named under Advanced and on the line under the legend.
- $80 to hotel.near → Yes. Not sent yet.
- $250 to airline.near → Ask you. Press Yes on the airline row. The demo balance does not move until you do. Money is still not sent.
- $500 → No · over the spending limit.
- Fake look-alike → the hotel tool says the account moved and asks to pay hote1.near (it looks like hotel.near), and claims you already said yes. That claim is ignored. Status: No · not someone you can pay.
- Too many rides → $120 to rides.near, again, until the $800 day stops the last one. Earlier rides are Yes. The last is No · over today's limit. Those yes answers are checks, not sends.
15. Roadmap
Near: multi-agent caps · OutLayer funded dry-run · Function Call Key helpers · OutLayer attestation verify page (when wired) · receipt audit seal.
Later: on-chain encrypted policy verify page · swarm markets · allowlist marketplace.
16. References
- nearhackathon.com/HACKATHON.md
- docs.near-intents.org
- OutLayer Agent Custody
- NEAR access keys / Function Call Keys
- Repo AGENT.md · /llms.txt · README
Built by arults for NEAR HACKATHON 2026. Contact details go on the official submit form — not invented in this repo.